Files
auto-login/popup.js
T

1673 lines
56 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
document.addEventListener("DOMContentLoaded", () => {
const $ = (id) => document.getElementById(id);
// i18n
const t = (key, ...args) => {
const msg = chrome.i18n.getMessage(key, args);
return msg || key;
};
// 遍历 data-i18n* 属性应用翻译;含嵌套标签的元素只替换首个文本节点
function applyI18n(root = document) {
root.querySelectorAll("[data-i18n]").forEach((el) => {
const key = el.dataset.i18n;
if (el.children.length === 0) {
const msg = chrome.i18n.getMessage(key);
if (msg) el.textContent = msg;
} else {
const msg = chrome.i18n.getMessage(key);
if (msg) {
const firstText = el.firstChild;
if (firstText && firstText.nodeType === Node.TEXT_NODE) {
firstText.nodeValue = msg;
}
}
}
});
root.querySelectorAll("[data-i18n-placeholder]").forEach((el) => {
const key = el.dataset.i18nPlaceholder;
const msg = chrome.i18n.getMessage(key);
if (msg) el.placeholder = msg;
});
root.querySelectorAll("[data-i18n-title]").forEach((el) => {
const key = el.dataset.i18nTitle;
const msg = chrome.i18n.getMessage(key);
if (msg) el.title = msg;
});
root.querySelectorAll("[data-i18n-aria]").forEach((el) => {
const key = el.dataset.i18nAria;
const msg = chrome.i18n.getMessage(key);
if (msg) el.setAttribute("aria-label", msg);
});
}
applyI18n();
const versionLabel = $("versionLabel");
if (versionLabel) versionLabel.textContent = "v" + chrome.runtime.getManifest().version;
const domainInput = $("domain");
const aliasInput = $("alias");
const usernameInput = $("username");
const passwordInput = $("password");
const autoSubmitSelect = $("autoSubmit");
const enabledCheckbox = $("enabled");
const delayMsInput = $("delayMs");
const usernameSelectorInput = $("usernameSelector");
const passwordSelectorInput = $("passwordSelector");
const submitSelectorInput = $("submitSelector");
const addBtn = $("addBtn");
const configList = $("configList");
const addSection = $("addSection");
const listSection = $("listSection");
const fillCurrentBtn = $("fillCurrent");
const fillDomainBtn = $("fillDomain");
const exportBtn = $("exportBtn");
const importBtn = $("importBtn");
const importFile = $("importFile");
const importBookmarkBtn = $("importBookmarkBtn");
const bookmarkModal = $("bookmarkModal");
const bookmarkList = $("bookmarkList");
const bookmarkSelectAll = $("bookmarkSelectAll");
const bookmarkSearch = $("bookmarkSearch");
const bookmarkCancel = $("bookmarkCancel");
const bookmarkConfirm = $("bookmarkConfirm");
const toast = $("toast");
// 编辑配置弹窗
const editConfigModal = $("editConfigModal");
const editConfigModalTitle = $("editConfigModalTitle");
const editDomainInput = $("editDomain");
const editUsernameInput = $("editUsername");
const editPasswordInput = $("editPassword");
const editAliasInput = $("editAlias");
const editAutoSubmitSelect = $("editAutoSubmit");
const editEnabledCheckbox = $("editEnabled");
const editDelayMsInput = $("editDelayMs");
const editUsernameSelectorInput = $("editUsernameSelector");
const editPasswordSelectorInput = $("editPasswordSelector");
const editSubmitSelectorInput = $("editSubmitSelector");
const editConfigCancel = $("editConfigCancel");
const editConfigSave = $("editConfigSave");
const editFillDomainBtn = $("editFillDomain");
let editModalEditingId = null;
const addEntryBtn = $("addEntryBtn");
const searchBtn = $("searchBtn");
const searchBox = $("searchBox");
const searchInput = $("searchInput");
const searchClear = $("searchClear");
const searchToggleBtn = $("searchToggleBtn");
let searchKeyword = "";
// 锁定界面
const lockScreen = $("lockScreen");
const mainContainer = $("mainContainer");
const lockTitle = $("lockTitle");
const lockHint = $("lockHint");
const lockPassword = $("lockPassword");
const lockPasswordConfirm = $("lockPasswordConfirm");
const lockConfirmGroup = $("lockConfirmGroup");
const lockSubmitBtn = $("lockSubmitBtn");
const lockWarn = $("lockWarn");
const lockBtn = $("lockBtn");
// 环境栏
const envSelect = $("envSelect");
const envManageBtn = $("envManageBtn");
const envManageModal = $("envManageModal");
const envManageList = $("envManageList");
const envManageWarn = $("envManageWarn");
const envManageCloseX = $("envManageCloseX");
const envManageAdd = $("envManageAdd");
const envModal = $("envModal");
const envModalTitle = $("envModalTitle");
const envNameInput = $("envNameInput");
const presetEnvTags = $("presetEnvTags");
const envModalWarn = $("envModalWarn");
const envModalCancel = $("envModalCancel");
const envModalCloseX = $("envModalCloseX");
const envModalConfirm = $("envModalConfirm");
const pwdModal = $("pwdModal");
const modalTitle = $("modalTitle");
const encryptToggle = $("encryptToggle");
const encryptToggleGroup = $("encryptToggleGroup");
const pwdFields = $("pwdFields");
const confirmGroup = $("confirmGroup");
const modalPwd = $("modalPwd");
const modalPwdConfirm = $("modalPwdConfirm");
const modalWarn = $("modalWarn");
const modalCancel = $("modalCancel");
const modalConfirm = $("modalConfirm");
const strengthBlock = $("strengthBlock");
const strengthBar = $("strengthBar");
const strengthLabel = $("strengthLabel");
const crackTime = $("crackTime");
let modalResolve = null;
// 密码强度估算:按 GPU 集群约 10^7 次/秒(PBKDF2-SHA256)计
const ATTACK_RATE = 1e7;
function estimateStrength(pwd) {
if (!pwd) return { entropy: 0, seconds: 0, level: "empty", label: t("strengthEmpty") };
let pool = 0;
if (/[a-z]/.test(pwd)) pool += 26;
if (/[A-Z]/.test(pwd)) pool += 26;
if (/[0-9]/.test(pwd)) pool += 10;
if (/[^a-zA-Z0-9]/.test(pwd)) pool += 32;
if (pool === 0) pool = 1;
const entropy = pwd.length * Math.log2(pool);
const combinations = Math.pow(2, entropy);
// 平均破解时间约为搜索空间的一半
const seconds = combinations / 2 / ATTACK_RATE;
let level, label;
if (seconds < 1) { level = "very-weak"; label = t("strengthVeryWeak"); }
else if (seconds < 3600) { level = "weak"; label = t("strengthWeak"); }
else if (seconds < 86400 * 30) { level = "medium"; label = t("strengthMedium"); }
else if (seconds < 86400 * 365 * 1000) { level = "strong"; label = t("strengthStrong"); }
else { level = "very-strong"; label = t("strengthVeryStrong"); }
return { entropy, seconds, level, label };
}
function formatTime(seconds) {
if (seconds < 1) return t("crackInstant");
if (seconds < 60) return t("crackSeconds", String(Math.round(seconds)));
if (seconds < 3600) return t("crackMinutes", String(Math.round(seconds / 60)));
if (seconds < 86400) return t("crackHours", String(Math.round(seconds / 3600)));
if (seconds < 86400 * 30) return t("crackDays", String(Math.round(seconds / 86400)));
if (seconds < 86400 * 365) return t("crackMonths", String(Math.round(seconds / 86400 / 30)));
const years = seconds / 86400 / 365;
if (years < 1000) return t("crackYears", String(Math.round(years)));
if (years < 1e6) return t("crackThousandYears", String(Math.round(years / 1000)));
if (years < 1e9) return t("crackMillionYears", String(Math.round(years / 1e6)));
if (years < 1e12) return t("crackBillionYears", String(Math.round(years / 1e9)));
return t("crackTrillionYears", (years / 1e12).toExponential(2));
}
function updateStrength(pwd) {
const { entropy, seconds, level, label } = estimateStrength(pwd);
// 5 档进度条宽度,对数缩放避免弱口令条太短看不见
const widthMap = {
"empty": 0,
"very-weak": 15,
"weak": 30,
"medium": 55,
"strong": 80,
"very-strong": 100,
};
strengthBar.style.width = widthMap[level] + "%";
strengthBar.className = "strength-bar-fg " + level;
strengthLabel.textContent = pwd ? t("strengthLabel", label, entropy.toFixed(1)) : t("strengthEmpty");
crackTime.textContent = pwd ? t("crackTimeLabel", formatTime(seconds)) : "";
}
modalPwd.addEventListener("input", () => {
if (strengthBlock.style.display !== "none") updateStrength(modalPwd.value);
});
// Web Crypto 工具:AES-GCM + PBKDF2
const enc = new TextEncoder();
const dec = new TextDecoder();
function bufToBase64(buf) {
const bytes = new Uint8Array(buf);
let bin = "";
for (let i = 0; i < bytes.length; i++) bin += String.fromCharCode(bytes[i]);
return btoa(bin);
}
function base64ToBuf(b64) {
const bin = atob(b64);
const bytes = new Uint8Array(bin.length);
for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);
return bytes.buffer;
}
async function deriveKey(password, salt, iterations) {
const keyMaterial = await crypto.subtle.importKey(
"raw",
enc.encode(password),
{ name: "PBKDF2" },
false,
["deriveKey"]
);
return crypto.subtle.deriveKey(
{ name: "PBKDF2", salt, iterations, hash: "SHA-256" },
keyMaterial,
{ name: "AES-GCM", length: 256 },
false,
["encrypt", "decrypt"]
);
}
async function encryptConfigs(configs, password) {
const salt = crypto.getRandomValues(new Uint8Array(16));
const iv = crypto.getRandomValues(new Uint8Array(12));
const iterations = 100000;
const key = await deriveKey(password, salt, iterations);
const plaintext = enc.encode(JSON.stringify(configs));
const ciphertext = await crypto.subtle.encrypt(
{ name: "AES-GCM", iv },
key,
plaintext
);
return {
algorithm: "AES-GCM",
iv: bufToBase64(iv),
ciphertext: bufToBase64(ciphertext),
kdf: {
algorithm: "PBKDF2",
iterations,
hash: "SHA-256",
salt: bufToBase64(salt)
}
};
}
async function decryptConfigs(cipher, password) {
const salt = new Uint8Array(base64ToBuf(cipher.kdf.salt));
const iv = new Uint8Array(base64ToBuf(cipher.iv));
const iterations = cipher.kdf.iterations || 100000;
const key = await deriveKey(password, salt, iterations);
const plainBuf = await crypto.subtle.decrypt(
{ name: "AES-GCM", iv },
key,
base64ToBuf(cipher.ciphertext)
);
return JSON.parse(dec.decode(plainBuf));
}
// 文件密码 modal:export 显示加密勾选 + 两次输入,import 只输入一次
function openPwdModal(mode) {
return new Promise((resolve) => {
modalResolve = resolve;
if (mode === "export") {
modalTitle.textContent = t("pwdModalExportTitle");
encryptToggleGroup.style.display = "";
confirmGroup.style.display = "";
strengthBlock.style.display = "";
encryptToggle.checked = true;
pwdFields.classList.remove("hidden");
modalPwd.value = "";
modalPwdConfirm.value = "";
modalWarn.textContent = t("pwdModalExportWarn");
} else {
modalTitle.textContent = t("pwdModalImportTitle");
encryptToggleGroup.style.display = "none";
confirmGroup.style.display = "none";
strengthBlock.style.display = "none";
pwdFields.classList.remove("hidden");
modalPwd.value = "";
modalPwdConfirm.value = "";
modalWarn.textContent = "";
}
pwdModal.classList.add("show");
setTimeout(() => modalPwd.focus(), 0);
});
}
function closePwdModal() {
pwdModal.classList.remove("show");
modalPwd.value = "";
modalPwdConfirm.value = "";
modalResolve = null;
updateStrength("");
}
encryptToggle.addEventListener("change", () => {
if (encryptToggle.checked) {
pwdFields.classList.remove("hidden");
updateStrength(modalPwd.value);
} else {
pwdFields.classList.add("hidden");
updateStrength("");
}
});
modalCancel.addEventListener("click", () => {
if (modalResolve) modalResolve(null);
closePwdModal();
});
modalConfirm.addEventListener("click", () => {
if (!modalResolve) return;
const isExport = modalTitle.textContent === t("pwdModalExportTitle");
if (isExport) {
if (encryptToggle.checked) {
const p1 = modalPwd.value;
const p2 = modalPwdConfirm.value;
if (!p1) {
modalWarn.textContent = t("warnFilePwdEmpty");
return;
}
if (p1 !== p2) {
modalWarn.textContent = t("warnFilePwdMismatch");
return;
}
modalResolve({ encrypt: true, password: p1 });
} else {
modalResolve({ encrypt: false, password: null });
}
} else {
const p = modalPwd.value;
if (!p) {
modalWarn.textContent = t("warnFilePwdEmpty");
return;
}
modalResolve({ encrypt: true, password: p });
}
closePwdModal();
});
modalPwd.addEventListener("keydown", (e) => {
if (e.key === "Enter") {
e.preventDefault();
if (confirmGroup.style.display === "none") {
modalConfirm.click();
} else if (modalPwdConfirm.value === "") {
modalPwdConfirm.focus();
} else {
modalConfirm.click();
}
}
});
modalPwdConfirm.addEventListener("keydown", (e) => {
if (e.key === "Enter") {
e.preventDefault();
modalConfirm.click();
}
});
function showToast(msg) {
toast.textContent = msg;
toast.classList.add("show");
setTimeout(() => toast.classList.remove("show"), 2000);
}
document.querySelectorAll(".toggle-pwd").forEach((btn) => {
btn.addEventListener("click", () => {
const target = $(btn.dataset.target);
if (target.type === "password") {
target.type = "text";
btn.classList.remove("eye-open");
btn.classList.add("eye-closed");
btn.textContent = "🙈";
} else {
target.type = "password";
btn.classList.remove("eye-closed");
btn.classList.add("eye-open");
btn.textContent = "👁";
}
});
});
// 表单内容实时存入草稿,弹窗关闭后不丢失
const draftFields = [
domainInput, aliasInput, usernameInput, passwordInput, autoSubmitSelect,
usernameSelectorInput, passwordSelectorInput, submitSelectorInput,
enabledCheckbox, delayMsInput,
];
draftFields.forEach((el) => {
el.addEventListener("input", saveDraft);
el.addEventListener("change", saveDraft);
});
// 环境数据层(EnvStore)
const {
loadEnvironments,
saveEnvironments,
getActiveEnv,
setActiveEnv,
createEnvironment,
renameEnvironment,
deleteEnvironment,
loadConfigs,
saveConfigs,
loadAllConfigs,
genId,
genCfgId,
} = EnvStore;
// isDomainMatch 由 utils.js 提供(全局函数)
const DRAFT_KEY = "formDraft";
function saveDraft() {
const draft = {
domain: domainInput.value,
alias: aliasInput.value,
username: usernameInput.value,
password: passwordInput.value,
autoSubmit: autoSubmitSelect.value,
usernameSelector: usernameSelectorInput.value,
passwordSelector: passwordSelectorInput.value,
submitSelector: submitSelectorInput.value,
enabled: enabledCheckbox.checked,
delayMs: delayMsInput.value,
};
chrome.storage.local.set({ [DRAFT_KEY]: draft });
}
async function loadDraft() {
const data = await chrome.storage.local.get(DRAFT_KEY);
const draft = data[DRAFT_KEY];
if (!draft) return;
domainInput.value = draft.domain || "";
aliasInput.value = draft.alias || "";
usernameInput.value = draft.username || "";
passwordInput.value = draft.password || "";
autoSubmitSelect.value = draft.autoSubmit || "true";
usernameSelectorInput.value = draft.usernameSelector || "";
passwordSelectorInput.value = draft.passwordSelector || "";
submitSelectorInput.value = draft.submitSelector || "";
enabledCheckbox.checked = draft.enabled !== false;
delayMsInput.value = draft.delayMs || "";
addBtn.textContent = t("addConfigBtn");
}
function clearDraft() {
chrome.storage.local.remove(DRAFT_KEY);
}
// 环境栏
let envModalResolve = null;
// mode: "create" | "rename"
function openEnvModal(mode, currentName = "") {
return new Promise((resolve) => {
envModalResolve = resolve;
if (mode === "create") {
envModalTitle.textContent = t("envCreateTitle");
envNameInput.value = "";
presetEnvTags.style.display = "flex";
} else {
envModalTitle.textContent = t("envRenameTitle");
envNameInput.value = currentName;
presetEnvTags.style.display = "flex";
}
envModalWarn.style.display = "none";
envModalWarn.textContent = "";
envModal.classList.add("show");
setTimeout(() => {
envNameInput.focus();
envNameInput.select();
}, 0);
});
}
function closeEnvModal() {
envModal.classList.remove("show");
envNameInput.value = "";
envModalWarn.style.display = "none";
envModalResolve = null;
}
presetEnvTags.querySelectorAll(".preset-tag").forEach((tag) => {
tag.addEventListener("click", () => {
envNameInput.value = tag.dataset.name;
envNameInput.focus();
});
});
function closeEnvModalWithResolve() {
if (envModalResolve) envModalResolve(null);
closeEnvModal();
}
envModalCancel.addEventListener("click", closeEnvModalWithResolve);
envModalCloseX.addEventListener("click", closeEnvModalWithResolve);
envModalConfirm.addEventListener("click", async () => {
if (!envModalResolve) return;
const name = envNameInput.value.trim();
if (!name) {
envModalWarn.textContent = t("envWarnEmpty");
envModalWarn.style.display = "block";
return;
}
if (name.length > 20) {
envModalWarn.textContent = t("envWarnTooLong");
envModalWarn.style.display = "block";
return;
}
envModalResolve(name);
closeEnvModal();
});
envNameInput.addEventListener("keydown", (e) => {
if (e.key === "Enter") {
e.preventDefault();
envModalConfirm.click();
}
});
async function renderEnvSelect() {
const { environments, activeEnvId } = await loadEnvironments();
envSelect.innerHTML = "";
environments.forEach((env) => {
const opt = document.createElement("option");
opt.value = env.id;
opt.textContent = `${env.name} (${env.configs?.length || 0})`;
if (env.id === activeEnvId) opt.selected = true;
envSelect.appendChild(opt);
});
}
envSelect.addEventListener("change", async () => {
await setActiveEnv(envSelect.value);
resetForm();
await renderConfigList();
showToast(t("toastSwitchEnv"));
});
// 环境管理对话框
function openEnvManageModal() {
envManageWarn.textContent = "";
envManageWarn.style.display = "none";
renderEnvManageList();
envManageModal.classList.add("show");
}
function closeEnvManageModal() {
envManageModal.classList.remove("show");
}
async function renderEnvManageList() {
const { environments, activeEnvId } = await loadEnvironments();
envManageList.innerHTML = "";
if (environments.length === 0) {
envManageList.innerHTML = `<div class="env-manage-empty">${t("envManageEmpty")}</div>`;
return;
}
environments.forEach((env) => {
const row = document.createElement("div");
row.className = "env-manage-row" + (env.id === activeEnvId ? " active" : "");
const nameSpan = document.createElement("span");
nameSpan.className = "env-manage-name";
const cnt = env.configs?.length || 0;
const activeMark = env.id === activeEnvId ? `<span class="env-manage-active-mark">${t("envCurrentMark")}</span>` : "";
nameSpan.innerHTML = `${escapeHtml(env.name)} <span class="env-manage-count">(${cnt})</span> ${activeMark}`;
const actions = document.createElement("div");
actions.className = "env-manage-actions";
const editBtn = document.createElement("button");
editBtn.className = "btn-icon";
editBtn.title = t("renameBtn");
editBtn.innerHTML = '<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 20h9"/><path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z"/></svg>';
editBtn.addEventListener("click", async () => {
const newName = await openEnvModal("rename", env.name);
if (!newName) return;
try {
await renameEnvironment(env.id, newName);
await renderEnvManageList();
await renderEnvSelect();
showToast(t("toastRenamed"));
} catch (err) {
showToast(err.message || t("toastRenameFail"));
}
});
const delBtn = document.createElement("button");
delBtn.className = "btn-icon";
delBtn.title = t("deleteBtn");
delBtn.innerHTML = '<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6"/><path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg>';
delBtn.addEventListener("click", async () => {
if (environments.length <= 1) {
showToast(t("toastKeepOneEnv"));
return;
}
const cnt2 = env.configs?.length || 0;
const msg = cnt2 > 0
? t("confirmDeleteEnvWithCfgs", env.name, String(cnt2))
: t("confirmDeleteEnv", env.name);
if (!confirm(msg)) return;
try {
await deleteEnvironment(env.id);
await renderEnvManageList();
await renderEnvSelect();
resetForm();
await renderConfigList();
showToast(t("toastEnvDeleted"));
} catch (err) {
showToast(err.message || t("toastDeleteFail"));
}
});
actions.appendChild(editBtn);
actions.appendChild(delBtn);
row.appendChild(nameSpan);
row.appendChild(actions);
envManageList.appendChild(row);
});
}
envManageBtn.addEventListener("click", openEnvManageModal);
envManageCloseX.addEventListener("click", closeEnvManageModal);
envManageAdd.addEventListener("click", async () => {
const name = await openEnvModal("create");
if (!name) return;
try {
await createEnvironment(name);
await renderEnvManageList();
await renderEnvSelect();
resetForm();
await renderConfigList();
showToast(t("toastEnvCreated", name));
} catch (err) {
showToast(err.message || t("toastCreateFail"));
}
});
function openConfigUrl(domain) {
if (!domain) return;
const url = /^https?:\/\//i.test(domain) ? domain : "http://" + domain;
window.open(url, "_blank", "noopener");
}
async function fetchTitleByDomain(domain) {
try {
const base = /^https?:\/\//i.test(domain) ? domain : "*://" + domain;
const patterns = [base];
if (!base.endsWith("*")) patterns.push(base + "/*");
const tabs = await chrome.tabs.query({ url: patterns });
for (const tab of tabs) {
if (tab.title) return tab.title;
}
} catch (e) {}
return null;
}
function closeAllMenus() {
document.querySelectorAll(".config-menu.open").forEach((m) => m.classList.remove("open"));
}
document.addEventListener("click", (e) => {
if (!e.target.closest(".config-actions")) closeAllMenus();
});
async function renderConfigList() {
const allConfigs = await loadConfigs();
configList.innerHTML = "";
// 无配置时禁用导出,此时表单在上、空状态的列表在下;有配置时反过来
const isEmpty = allConfigs.length === 0;
addSection.classList.toggle("collapsed", !isEmpty);
exportBtn.disabled = isEmpty;
exportBtn.classList.toggle("btn-disabled", isEmpty);
const parent = addSection.parentElement;
const addIdx = Array.prototype.indexOf.call(parent.children, addSection);
const listIdx = Array.prototype.indexOf.call(parent.children, listSection);
if (isEmpty && addIdx > listIdx) {
parent.insertBefore(addSection, listSection);
} else if (!isEmpty && listIdx > addIdx) {
parent.insertBefore(listSection, addSection);
}
if (isEmpty) {
configList.innerHTML = `
<div class="empty-state">
<div class="empty-icon">
<svg viewBox="0 0 24 24" width="56" height="56" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round">
<rect x="3" y="5" width="18" height="14" rx="2"/>
<path d="M3 10h18"/>
<circle cx="7" cy="14.5" r="0.6" fill="currentColor"/>
<path d="M11 14.5h6"/>
</svg>
</div>
<div class="empty-text">${t("emptyStateTitle")}</div>
<div class="empty-sub">${t("emptyStateSub")}</div>
</div>
`;
return;
}
// 搜索过滤:支持域名、用户名、别名模糊检索
const kw = searchKeyword.trim().toLowerCase();
const configs = kw
? allConfigs.filter((c) =>
(c.domain || "").toLowerCase().includes(kw) ||
(c.username || "").toLowerCase().includes(kw) ||
(c.alias || "").toLowerCase().includes(kw))
: allConfigs;
if (configs.length === 0) {
configList.innerHTML = `
<div class="empty-state">
<div class="empty-text">${t("noSearchResult")}</div>
</div>
`;
return;
}
let draggedConfigId = null;
configs.forEach((cfg) => {
const item = document.createElement("div");
item.className = "config-item" + (cfg.enabled === false ? " config-disabled" : "");
if (!kw) {
item.draggable = true;
item.addEventListener("dragstart", (e) => {
draggedConfigId = cfg.id;
item.classList.add("dragging");
e.dataTransfer.effectAllowed = "move";
});
item.addEventListener("dragend", () => {
draggedConfigId = null;
item.classList.remove("dragging");
configList.querySelectorAll(".config-item").forEach((el) => el.classList.remove("drag-over"));
});
item.addEventListener("dragover", (e) => {
e.preventDefault();
e.dataTransfer.dropEffect = "move";
if (cfg.id !== draggedConfigId) {
item.classList.add("drag-over");
}
});
item.addEventListener("dragleave", () => {
item.classList.remove("drag-over");
});
item.addEventListener("drop", async (e) => {
e.preventDefault();
item.classList.remove("drag-over");
if (!draggedConfigId || draggedConfigId === cfg.id) return;
const fromIdx = allConfigs.findIndex((c) => c.id === draggedConfigId);
const toIdx = allConfigs.findIndex((c) => c.id === cfg.id);
if (fromIdx === -1 || toIdx === -1) return;
const [moved] = allConfigs.splice(fromIdx, 1);
allConfigs.splice(toIdx, 0, moved);
await saveConfigs(allConfigs);
renderConfigList();
});
}
const info = document.createElement("div");
info.className = "config-info";
const displayName = escapeHtml(cfg.alias || cfg.domain);
const enabledTag = cfg.enabled === false
? `<span class="tag-disabled">${t("tagDisabled")}</span>`
: "";
info.innerHTML = `
<div class="config-domain">${displayName} ${enabledTag}</div>
`;
const actions = document.createElement("div");
actions.className = "config-actions";
const isDisabled = cfg.enabled === false;
// 编辑(图标按钮)
const editBtn = document.createElement("button");
editBtn.className = "btn-icon";
editBtn.title = t("editBtn");
editBtn.innerHTML = '<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 20h9"/><path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z"/></svg>';
editBtn.addEventListener("click", (e) => {
e.stopPropagation();
editModalEditingId = cfg.id;
editConfigModalTitle.textContent = t("editConfigTitle");
editConfigSave.textContent = t("updateConfigBtn");
editDomainInput.value = cfg.domain;
editAliasInput.value = cfg.alias || "";
editUsernameInput.value = cfg.username;
editPasswordInput.value = cfg.password;
editAutoSubmitSelect.value = String(cfg.autoSubmit);
editUsernameSelectorInput.value = cfg.usernameSelector || "";
editPasswordSelectorInput.value = cfg.passwordSelector || "";
editSubmitSelectorInput.value = cfg.submitSelector || "";
editEnabledCheckbox.checked = cfg.enabled !== false;
editDelayMsInput.value = cfg.delayMs || "";
editConfigModal.classList.add("show");
editDomainInput.focus();
});
// 三个小点下拉菜单(收起禁用/删除)
const moreBtn = document.createElement("button");
moreBtn.className = "btn-icon";
moreBtn.title = t("moreActionsTitle");
moreBtn.innerHTML = '<svg viewBox="0 0 24 24" width="14" height="14" fill="currentColor"><circle cx="12" cy="5" r="1.6"/><circle cx="12" cy="12" r="1.6"/><circle cx="12" cy="19" r="1.6"/></svg>';
moreBtn.addEventListener("click", (e) => {
e.stopPropagation();
const open = menu.classList.contains("open");
closeAllMenus();
if (!open) menu.classList.add("open");
});
const menu = document.createElement("div");
menu.className = "config-menu";
const toggleItem = document.createElement("button");
toggleItem.className = "config-menu-item";
toggleItem.textContent = isDisabled ? t("toggleEnable") : t("toggleDisable");
toggleItem.addEventListener("click", async (e) => {
e.stopPropagation();
closeAllMenus();
const updated = configs.map((c) =>
c.id === cfg.id ? { ...c, enabled: c.enabled === false } : c
);
await saveConfigs(updated);
renderConfigList();
renderEnvSelect();
if (envManageModal.classList.contains("show")) renderEnvManageList();
showToast(isDisabled ? t("toastEnabled") : t("toastDisabled"));
});
const delItem = document.createElement("button");
delItem.className = "config-menu-item danger";
delItem.textContent = t("deleteBtn");
delItem.addEventListener("click", async (e) => {
e.stopPropagation();
closeAllMenus();
const updated = configs.filter((c) => c.id !== cfg.id);
await saveConfigs(updated);
showToast(t("toastDeleted"));
renderConfigList();
renderEnvSelect();
if (envManageModal.classList.contains("show")) renderEnvManageList();
});
menu.appendChild(toggleItem);
menu.appendChild(delItem);
actions.appendChild(editBtn);
actions.appendChild(moreBtn);
actions.appendChild(menu);
// 点击整项 → 新窗口打开
item.addEventListener("click", () => openConfigUrl(cfg.domain));
item.appendChild(info);
item.appendChild(actions);
configList.appendChild(item);
});
}
function resetForm() {
domainInput.value = "";
aliasInput.value = "";
usernameInput.value = "";
passwordInput.value = "";
autoSubmitSelect.value = "true";
usernameSelectorInput.value = "";
passwordSelectorInput.value = "";
submitSelectorInput.value = "";
enabledCheckbox.checked = true;
delayMsInput.value = "";
addBtn.textContent = t("addConfigBtn");
clearDraft();
}
addBtn.addEventListener("click", async () => {
const domain = domainInput.value.trim();
const username = usernameInput.value.trim();
const password = passwordInput.value;
if (!domain || !username || !password) {
showToast(t("toastRequired"));
return;
}
const config = {
domain,
alias: aliasInput.value.trim() || await fetchTitleByDomain(domain) || null,
username,
password,
autoSubmit: autoSubmitSelect.value === "true",
usernameSelector: usernameSelectorInput.value.trim() || null,
passwordSelector: passwordSelectorInput.value.trim() || null,
submitSelector: submitSelectorInput.value.trim() || null,
enabled: enabledCheckbox.checked,
delayMs: delayMsInput.value ? Number(delayMsInput.value) : null,
};
const configs = await loadConfigs();
config.id = Date.now().toString(36) + Math.random().toString(36).slice(2, 8);
configs.push(config);
showToast(t("toastAdded"));
await saveConfigs(configs);
resetForm();
renderConfigList();
renderEnvSelect();
if (envManageModal.classList.contains("show")) renderEnvManageList();
});
// 编辑配置弹窗:保存
editConfigSave.addEventListener("click", async () => {
const domain = editDomainInput.value.trim();
const username = editUsernameInput.value.trim();
const password = editPasswordInput.value;
if (!domain || !username || !password) {
showToast(t("toastRequired"));
return;
}
const config = {
domain,
alias: editAliasInput.value.trim() || await fetchTitleByDomain(domain) || null,
username,
password,
autoSubmit: editAutoSubmitSelect.value === "true",
usernameSelector: editUsernameSelectorInput.value.trim() || null,
passwordSelector: editPasswordSelectorInput.value.trim() || null,
submitSelector: editSubmitSelectorInput.value.trim() || null,
enabled: editEnabledCheckbox.checked,
delayMs: editDelayMsInput.value ? Number(editDelayMsInput.value) : null,
};
const configs = await loadConfigs();
if (editModalEditingId) {
const idx = configs.findIndex((c) => c.id === editModalEditingId);
if (idx !== -1) {
config.id = editModalEditingId;
configs[idx] = config;
}
showToast(t("toastUpdated"));
} else {
config.id = Date.now().toString(36) + Math.random().toString(36).slice(2, 8);
configs.push(config);
showToast(t("toastAdded"));
}
await saveConfigs(configs);
editConfigModal.classList.remove("show");
editModalEditingId = null;
renderConfigList();
renderEnvSelect();
if (envManageModal.classList.contains("show")) renderEnvManageList();
});
// 添加配置入口按钮:打开新增弹窗
addEntryBtn.addEventListener("click", () => {
editModalEditingId = null;
editConfigModalTitle.textContent = t("addConfigTitle");
editConfigSave.textContent = t("okBtn");
editDomainInput.value = "";
editUsernameInput.value = "";
editPasswordInput.value = "";
editAliasInput.value = "";
editAutoSubmitSelect.value = "true";
editEnabledCheckbox.checked = true;
editDelayMsInput.value = "";
editUsernameSelectorInput.value = "";
editPasswordSelectorInput.value = "";
editSubmitSelectorInput.value = "";
editConfigModal.classList.add("show");
editDomainInput.focus();
});
// 搜索按钮:展开内联搜索框
searchBtn.addEventListener("click", () => {
searchBtn.hidden = true;
searchBox.hidden = false;
searchInput.focus();
});
// 搜索框内搜索图标:收起回图标按钮
searchToggleBtn.addEventListener("click", () => {
searchInput.value = "";
searchKeyword = "";
searchClear.hidden = true;
searchBox.hidden = true;
searchBtn.hidden = false;
renderConfigList();
});
// 搜索输入:实时过滤
searchInput.addEventListener("input", () => {
searchKeyword = searchInput.value;
searchClear.hidden = !searchKeyword;
renderConfigList();
});
// 清除搜索
searchClear.addEventListener("click", () => {
searchInput.value = "";
searchKeyword = "";
searchClear.hidden = true;
renderConfigList();
searchInput.focus();
});
// 编辑配置弹窗:取消
const editConfigCloseX = $("editConfigCloseX");
function closeEditConfigModal() {
editConfigModal.classList.remove("show");
editModalEditingId = null;
}
editConfigCancel.addEventListener("click", closeEditConfigModal);
editConfigCloseX.addEventListener("click", closeEditConfigModal);
// 点击遮罩关闭编辑弹窗
editConfigModal.addEventListener("click", (e) => {
if (e.target === editConfigModal) {
editConfigModal.classList.remove("show");
editModalEditingId = null;
}
});
fillCurrentBtn.addEventListener("click", async () => {
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
if (!tab) {
showToast(t("toastNoTab"));
return;
}
// 跨所有环境查找匹配当前标签页的配置
const allConfigs = await loadAllConfigs();
const matched = allConfigs.filter((c) => isDomainMatch(tab.url, c.domain));
if (matched.length === 0) {
showToast(t("toastNoMatch"));
return;
}
if (matched.length === 1) {
try {
const resp = await chrome.tabs.sendMessage(tab.id, {
action: "manualFill",
config: matched[0],
});
if (resp && resp.success) {
showToast(t("toastFilled"));
} else if (resp && resp.diag) {
const d = resp.diag;
showToast(`input=${d.total} pwd=${d.pwd} form=${d.forms} | ${d.sample.join(", ")}`);
} else {
showToast(t("toastFilledNoForm"));
}
} catch (err) {
// content script 未注入:常见于安装扩展前已打开的页面
if (/Could not establish connection|Receiving end does not exist/i.test(err.message)) {
showToast(t("toastScriptNotInjected"));
} else {
showToast(t("toastSendFail", err.message || String(err)));
}
}
return;
}
try {
await chrome.tabs.sendMessage(tab.id, {
action: "manualFillMulti",
configs: matched,
tabUrl: tab.url,
});
showToast(t("toastMultiMatch", String(matched.length)));
} catch (err) {
if (/Could not establish connection|Receiving end does not exist/i.test(err.message)) {
showToast(t("toastScriptNotInjected"));
} else {
showToast(t("toastSendFail", err.message || String(err)));
}
}
});
fillDomainBtn.addEventListener("click", async () => {
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
if (!tab || !tab.url) {
showToast(t("toastNoTab"));
return;
}
try {
const u = new URL(tab.url);
const host = u.host;
if (!host) {
showToast(t("toastNoDomain"));
return;
}
const value = u.pathname && u.pathname !== "/" ? host + u.pathname : host;
domainInput.value = value;
if (tab.title) aliasInput.value = tab.title;
saveDraft();
showToast(t("toastDomainFilled", value));
} catch {
showToast(t("toastUrlParseFail"));
}
});
editFillDomainBtn.addEventListener("click", async () => {
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
if (!tab || !tab.url) {
showToast(t("toastNoTab"));
return;
}
try {
const hostname = new URL(tab.url).hostname;
if (!hostname) {
showToast(t("toastNoDomain"));
return;
}
editDomainInput.value = hostname;
showToast(t("toastDomainFilled", hostname));
} catch {
showToast(t("toastUrlParseFail"));
}
});
// 导出整个 environments 结构(含环境名)
exportBtn.addEventListener("click", async () => {
const { environments } = await loadEnvironments();
const totalConfigs = environments.reduce((s, e) => s + (e.configs?.length || 0), 0);
if (totalConfigs === 0) {
showToast(t("toastNoExportable"));
return;
}
const choice = await openPwdModal("export");
if (!choice) {
showToast(t("toastExportCancelled"));
return;
}
const stamp = new Date().toISOString().replace(/[:T]/g, "").slice(0, 15);
let payload;
let filename;
if (choice.encrypt) {
const cipher = await encryptConfigs(environments, choice.password);
payload = {
app: "AutoLoginManager",
version: 3,
exportedAt: new Date().toISOString(),
encrypted: true,
cipher,
};
filename = `auto-login-configs-${stamp}.enc.json`;
} else {
payload = {
app: "AutoLoginManager",
version: 3,
exportedAt: new Date().toISOString(),
encrypted: false,
environments,
};
filename = `auto-login-configs-${stamp}.json`;
}
const blob = new Blob([JSON.stringify(payload, 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);
showToast(t("toastExported", String(environments.length), String(totalConfigs), choice.encrypt ? t("encryptedMark") : ""));
});
// 导入配置:兼容 v1(数组) / v2(configs) / v3(environments),加密则弹口令 modal
importBtn.addEventListener("click", () => importFile.click());
importFile.addEventListener("change", async (e) => {
const file = e.target.files[0];
if (!file) return;
try {
const text = await file.text();
const data = JSON.parse(text);
let incomingEnvironments = null;
let incomingConfigs = null;
// 1. v1: 直接数组格式
if (Array.isArray(data)) {
incomingConfigs = data;
}
// 2. 加密格式
else if (data?.encrypted === true && data?.cipher) {
const choice = await openPwdModal("import");
if (!choice) {
showToast(t("toastImportCancelled"));
importFile.value = "";
return;
}
try {
const decrypted = await decryptConfigs(data.cipher, choice.password);
// 解密后可能是 v3 (environments 数组/对象) 或 v1 (configs 数组)
if (Array.isArray(decrypted)) {
// 区分环境数组(v3, 元素含 configs)与配置数组(v1, 元素含 domain)
if (decrypted.some((e) => e && Array.isArray(e.configs))) {
incomingEnvironments = decrypted;
} else {
incomingConfigs = decrypted;
}
} else if (Array.isArray(decrypted?.environments)) {
incomingEnvironments = decrypted.environments;
} else if (Array.isArray(decrypted?.configs)) {
incomingConfigs = decrypted.configs;
} else {
showToast(t("toastInvalidFormat"));
importFile.value = "";
return;
}
} catch (err) {
showToast(t("toastDecryptFail"));
importFile.value = "";
return;
}
}
// 3. v3 明文:含 environments
else if (Array.isArray(data?.environments)) {
incomingEnvironments = data.environments;
}
// 4. v2 明文:含 configs
else if (Array.isArray(data?.configs)) {
incomingConfigs = data.configs;
} else {
showToast(t("toastFileInvalid"));
importFile.value = "";
return;
}
const { environments: existingEnvs, activeEnvId } = await loadEnvironments();
// 分支 A:导入 v3 环境结构 —— 按"环境名"匹配,已存在的环境跳过,新的环境整批导入
if (incomingEnvironments) {
const existingNames = new Set(existingEnvs.map((e) => e.name));
let addedEnvs = 0;
let addedCfgs = 0;
let skippedCfgs = 0;
for (const env of incomingEnvironments) {
if (!env || !env.name) {
skippedCfgs += (env?.configs?.length) || 0;
continue;
}
if (existingNames.has(env.name)) {
// 同名环境:跳过整个环境
skippedCfgs += (env.configs?.length) || 0;
continue;
}
existingNames.add(env.name);
const newEnv = {
id: genId(),
name: env.name,
configs: [],
};
for (const cfg of (env.configs || [])) {
if (!cfg || !cfg.domain || !cfg.username || !cfg.password) {
skippedCfgs++;
continue;
}
newEnv.configs.push({
id: cfg.id || genCfgId(),
domain: cfg.domain,
alias: cfg.alias || null,
username: cfg.username,
password: cfg.password,
autoSubmit: cfg.autoSubmit !== false,
usernameSelector: cfg.usernameSelector || null,
passwordSelector: cfg.passwordSelector || null,
submitSelector: cfg.submitSelector || null,
enabled: cfg.enabled !== false,
delayMs: cfg.delayMs || null,
});
addedCfgs++;
}
existingEnvs.push(newEnv);
addedEnvs++;
}
await saveEnvironments(existingEnvs, activeEnvId);
await renderEnvSelect();
await renderConfigList();
showToast(t("toastImportedEnvs", String(addedEnvs), String(addedCfgs), String(skippedCfgs)));
return;
}
// 分支 B:导入 v1/v2 配置数组 —— 合并到当前激活环境,相同 domain 跳过
const curEnv = existingEnvs.find((e) => e.id === activeEnvId) || existingEnvs[0];
const seenDomains = new Set(curEnv.configs.map((c) => c.domain));
let added = 0;
let skipped = 0;
for (const cfg of incomingConfigs) {
if (!cfg || !cfg.domain || !cfg.username || !cfg.password) {
skipped++;
continue;
}
if (seenDomains.has(cfg.domain)) {
skipped++;
continue;
}
seenDomains.add(cfg.domain);
curEnv.configs.push({
id: cfg.id || genCfgId(),
domain: cfg.domain,
alias: cfg.alias || null,
username: cfg.username,
password: cfg.password,
autoSubmit: cfg.autoSubmit !== false,
usernameSelector: cfg.usernameSelector || null,
passwordSelector: cfg.passwordSelector || null,
submitSelector: cfg.submitSelector || null,
enabled: cfg.enabled !== false,
delayMs: cfg.delayMs || null,
});
added++;
}
await saveEnvironments(existingEnvs);
await renderConfigList();
showToast(t("toastImportedCfgs", String(added), String(skipped)));
} catch (err) {
showToast(t("toastImportFail", err.message || "parse error"));
} finally {
importFile.value = "";
}
});
// 从书签导入
function flattenBookmarks(nodes, out) {
for (const node of nodes) {
if (node.url) {
out.push({ title: node.title || node.url, url: node.url });
}
if (node.children) {
flattenBookmarks(node.children, out);
}
}
}
function urlToDomain(url) {
try {
const u = new URL(url);
let domain = u.host + u.pathname;
if (domain.endsWith("/") && u.pathname !== "/") domain = domain.replace(/\/$/, "");
if (u.pathname === "/") domain = u.host;
return domain;
} catch (e) {
return null;
}
}
let bookmarkItems = [];
function renderBookmarkList(filter) {
const kw = (filter || "").trim().toLowerCase();
bookmarkList.innerHTML = "";
const items = kw
? bookmarkItems.filter((b) => b.title.toLowerCase().includes(kw) || b.url.toLowerCase().includes(kw))
: bookmarkItems;
const frag = document.createDocumentFragment();
for (const b of items) {
const row = document.createElement("label");
row.className = "bookmark-item";
const cb = document.createElement("input");
cb.type = "checkbox";
cb.dataset.url = b.url;
const text = document.createElement("span");
text.className = "bookmark-item-text";
text.textContent = b.title;
const sub = document.createElement("span");
sub.className = "bookmark-item-url";
sub.textContent = b.url;
row.appendChild(cb);
row.appendChild(text);
row.appendChild(sub);
frag.appendChild(row);
}
bookmarkList.appendChild(frag);
updateBookmarkSelectAllState();
}
function updateBookmarkSelectAllState() {
const cbs = bookmarkList.querySelectorAll("input[type=checkbox]");
const checked = bookmarkList.querySelectorAll("input[type=checkbox]:checked");
bookmarkSelectAll.checked = cbs.length > 0 && cbs.length === checked.length;
bookmarkConfirm.disabled = checked.length === 0;
}
importBookmarkBtn.addEventListener("click", async () => {
try {
const tree = await chrome.bookmarks.getTree();
bookmarkItems = [];
flattenBookmarks(tree, bookmarkItems);
bookmarkSearch.value = "";
bookmarkSelectAll.checked = false;
renderBookmarkList("");
bookmarkModal.classList.add("show");
} catch (e) {
showToast(t("bookmarkLoadFail"));
}
});
bookmarkSearch.addEventListener("input", () => {
renderBookmarkList(bookmarkSearch.value);
});
bookmarkList.addEventListener("change", () => {
updateBookmarkSelectAllState();
});
bookmarkSelectAll.addEventListener("change", () => {
const cbs = bookmarkList.querySelectorAll("input[type=checkbox]");
cbs.forEach((cb) => (cb.checked = bookmarkSelectAll.checked));
bookmarkConfirm.disabled = !bookmarkSelectAll.checked;
});
bookmarkCancel.addEventListener("click", () => {
bookmarkModal.classList.remove("show");
});
bookmarkModal.addEventListener("click", (e) => {
if (e.target === bookmarkModal) bookmarkModal.classList.remove("show");
});
bookmarkConfirm.addEventListener("click", async () => {
const checked = bookmarkList.querySelectorAll("input[type=checkbox]:checked");
if (!checked.length) return;
const configs = await loadConfigs();
const existingDomains = new Set(configs.map((c) => c.domain));
let added = 0;
let skipped = 0;
for (const cb of checked) {
const domain = urlToDomain(cb.dataset.url);
if (!domain || existingDomains.has(domain)) {
skipped++;
continue;
}
existingDomains.add(domain);
configs.push({
id: Date.now().toString(36) + Math.random().toString(36).slice(2, 8),
domain,
alias: cb.parentElement.querySelector(".bookmark-item-text").textContent || null,
username: "",
password: "",
autoSubmit: true,
usernameSelector: null,
passwordSelector: null,
submitSelector: null,
enabled: true,
delayMs: null,
});
added++;
}
await saveConfigs(configs);
bookmarkModal.classList.remove("show");
renderConfigList();
renderEnvSelect();
showToast(t("toastBookmarkImported", String(added), String(skipped)));
});
// 消息发送:SW 刚休眠唤醒时首次 sendMessage 会失败,需重试
function sendMessage(message, retries = 3) {
return new Promise((resolve, reject) => {
let attempt = 0;
const trySend = () => {
attempt++;
// 扩展上下文已失效(扩展被重载/更新),刷新页面恢复
if (!chrome.runtime?.id) {
location.reload();
reject(new Error(t("toastContextReload")));
return;
}
chrome.runtime.sendMessage(message, (resp) => {
const err = chrome.runtime.lastError;
if (err) {
// 接收端不存在:SW 还没注册 listener,等 200ms 重试
if (attempt < retries && /Receiving end does not exist/i.test(err.message)) {
setTimeout(trySend, 200 * attempt);
return;
}
reject(new Error(err.message));
return;
}
resolve(resp);
});
};
trySend();
});
}
// 锁定 / 解锁
let lockMode = "setup"; // "setup" | "unlock"
let lockAfterSetup = false;
function showLockScreen(mode) {
lockMode = mode;
lockScreen.style.display = "flex";
mainContainer.style.display = "none";
lockPassword.value = "";
lockPasswordConfirm.value = "";
lockWarn.textContent = "";
lockWarn.style.display = "none";
if (mode === "setup") {
lockTitle.textContent = t("lockSetupTitle");
lockHint.textContent = t("lockSetupHint");
lockConfirmGroup.style.display = "block";
lockSubmitBtn.textContent = t("lockSetupBtn");
} else {
lockTitle.textContent = t("lockUnlockTitle");
lockHint.textContent = t("lockUnlockHint");
lockConfirmGroup.style.display = "none";
lockSubmitBtn.textContent = t("lockUnlockBtn");
}
setTimeout(() => lockPassword.focus(), 50);
}
function showMainUI() {
lockScreen.style.display = "none";
mainContainer.style.display = "flex";
}
async function initLockState() {
try {
const resp = await sendMessage({ action: "getVaultStatus" });
lockBtn.hidden = false;
if (!resp.hasVault) {
// 无 vault:明文模式,直接进入,不锁定
showMainUI();
loadDraft();
renderEnvSelect();
renderConfigList();
} else if (!resp.unlocked) {
showLockScreen("unlock");
} else {
showMainUI();
loadDraft();
renderEnvSelect();
renderConfigList();
}
} catch (err) {
console.warn("[AutoLogin]", t("logGetLockStatusFail"), err);
lockBtn.hidden = false;
showMainUI();
loadDraft();
renderEnvSelect();
renderConfigList();
}
}
lockSubmitBtn.addEventListener("click", async () => {
const pwd = lockPassword.value;
if (!pwd) {
lockWarn.textContent = t("lockWarnEmpty");
lockWarn.style.display = "block";
return;
}
if (lockMode === "setup") {
if (pwd.length < 6) {
lockWarn.textContent = t("lockWarnShort");
lockWarn.style.display = "block";
return;
}
if (pwd !== lockPasswordConfirm.value) {
lockWarn.textContent = t("lockWarnMismatch");
lockWarn.style.display = "block";
return;
}
try {
await sendMessage({ action: "setupVault", password: pwd });
showToast(t("toastPasswordSet"));
if (lockAfterSetup) {
lockAfterSetup = false;
await sendMessage({ action: "lock" });
showLockScreen("unlock");
showToast(t("toastLocked"));
} else {
showMainUI();
loadDraft();
renderEnvSelect();
renderConfigList();
}
} catch (err) {
lockWarn.textContent = t("lockWarnSetupFail", err.message || String(err));
lockWarn.style.display = "block";
}
} else {
try {
const resp = await sendMessage({ action: "unlock", password: pwd });
if (resp.success) {
showToast(t("toastUnlocked"));
showMainUI();
loadDraft();
renderEnvSelect();
renderConfigList();
} else {
lockWarn.textContent = resp.error || t("lockWarnUnlockFail", "");
lockWarn.style.display = "block";
}
} catch (err) {
lockWarn.textContent = t("lockWarnUnlockFail", err.message || String(err));
lockWarn.style.display = "block";
}
}
});
lockPassword.addEventListener("keydown", (e) => {
if (e.key === "Enter") lockSubmitBtn.click();
});
lockPasswordConfirm.addEventListener("keydown", (e) => {
if (e.key === "Enter") lockSubmitBtn.click();
});
lockBtn.addEventListener("click", async () => {
const resp = await sendMessage({ action: "getVaultStatus" }).catch(() => null);
if (resp && !resp.hasVault) {
// 明文模式:先设密码再锁定
lockAfterSetup = true;
showLockScreen("setup");
return;
}
if (!confirm(t("lockConfirmPrompt"))) return;
await sendMessage({ action: "lock" });
showLockScreen("unlock");
showToast(t("toastLocked"));
});
// 弹窗内的点击视为用户活动,重置 background 自动锁定计时
document.addEventListener("click", () => {
if (lockScreen.style.display === "none") {
sendMessage({ action: "ping" }, 1).catch(() => {});
}
});
initLockState();
});