no message

This commit is contained in:
yinjun.chen1
2026-09-14 14:40:59 +08:00
parent 1eef16a332
commit 54b2a470c2
5 changed files with 385 additions and 119 deletions
+6
View File
@@ -63,8 +63,13 @@
await storage().set({ [ACTIVE_ENV_KEY]: envId });
}
const MAX_ENVIRONMENTS = 5;
async function createEnvironment(name) {
const { environments } = await loadEnvironments();
if (environments.length >= MAX_ENVIRONMENTS) {
throw new Error(`最多 ${MAX_ENVIRONMENTS} 个环境`);
}
const env = { id: genId(), name, configs: [] };
environments.push(env);
await saveEnvironments(environments, env.id);
@@ -133,6 +138,7 @@
return {
ENV_KEY,
ACTIVE_ENV_KEY,
MAX_ENVIRONMENTS,
loadEnvironments,
saveEnvironments,
getActiveEnv,
+139 -17
View File
@@ -386,6 +386,13 @@ h2 {
justify-content: center;
}
.btn-lock {
position: fixed;
top: 8px;
right: 8px;
z-index: 10;
}
.form-group label {
display: block;
margin-bottom: 4px;
@@ -422,57 +429,59 @@ h2 {
font-size: 11px;
}
.selector-help {
.advanced-options {
background: #16213e;
border: 1px solid #0f3460;
border-radius: 6px;
padding: 8px 10px;
padding: 10px 12px;
margin-bottom: 12px;
}
.selector-help summary {
.advanced-options summary {
cursor: pointer;
color: #a0a0b0;
font-size: 12px;
user-select: none;
list-style: none;
font-weight: 500;
}
.selector-help summary::-webkit-details-marker {
.advanced-options summary::-webkit-details-marker {
display: none;
}
.selector-help summary::before {
content: "▶ ";
.advanced-options summary::before {
content: "▶";
color: #e94560;
font-size: 10px;
margin-right: 10px;
transition: transform 0.2s;
display: inline-block;
}
.selector-help[open] summary::before {
content: "▼ ";
.advanced-options[open] summary::before {
content: "▼";
}
.selector-help-body {
margin-top: 10px;
padding-top: 8px;
.advanced-options-body {
margin-top: 12px;
padding-top: 10px;
border-top: 1px dashed #0f3460;
}
.selector-help-body p {
.advanced-options-body p {
font-size: 11px;
line-height: 1.6;
color: #a0a0b0;
margin-bottom: 6px;
}
.selector-help-body .help-tip {
.advanced-options-body .help-tip {
color: #e94560;
font-weight: 500;
}
.selector-help-body code {
.advanced-options-body code {
background: #0f1d3a;
padding: 1px 4px;
border-radius: 3px;
@@ -481,7 +490,7 @@ h2 {
font-family: Consolas, Monaco, monospace;
}
.selector-help-body kbd {
.advanced-options-body kbd {
background: #0f1d3a;
border: 1px solid #0f3460;
border-radius: 3px;
@@ -491,7 +500,7 @@ h2 {
font-family: Consolas, Monaco, monospace;
}
.selector-help-body .icon-arrow {
.advanced-options-body .icon-arrow {
display: inline-block;
padding: 1px 5px;
background: #0f3460;
@@ -500,7 +509,7 @@ h2 {
font-size: 11px;
}
.selector-help-body .rule-tag {
.advanced-options-body .rule-tag {
display: inline-block;
padding: 1px 5px;
margin: 0 2px;
@@ -512,6 +521,14 @@ h2 {
font-family: Consolas, Monaco, monospace;
}
.selector-tutorial {
margin: 8px 0 10px;
padding: 8px 10px;
background: rgba(15, 52, 96, 0.25);
border-left: 3px solid #0f3460;
border-radius: 4px;
}
.password-wrapper {
display: flex;
gap: 6px;
@@ -697,6 +714,47 @@ h2 {
font-size: 13px;
}
/* 空状态:缺省图标 */
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 32px 16px 24px;
text-align: center;
}
.empty-icon {
color: #0f3460;
margin-bottom: 14px;
opacity: 0.85;
}
.empty-icon svg {
display: block;
}
.empty-text {
font-size: 14px;
color: #a0a0b0;
margin-bottom: 6px;
font-weight: 500;
}
.empty-sub {
font-size: 11px;
color: #555;
line-height: 1.6;
}
/* 禁用按钮 */
.btn-io.btn-disabled,
.btn-io:disabled {
opacity: 0.4;
cursor: not-allowed;
pointer-events: none;
}
.toast {
position: fixed;
bottom: 10px;
@@ -810,6 +868,70 @@ h2 {
flex: 1;
}
/* 环境管理对话框列表 */
.env-manage-list {
display: flex;
flex-direction: column;
gap: 6px;
max-height: 280px;
overflow-y: auto;
margin-bottom: 8px;
}
.env-manage-row {
display: flex;
align-items: center;
justify-content: space-between;
padding: 8px 10px;
background: #16213e;
border: 1px solid #0f3460;
border-radius: 4px;
}
.env-manage-row.active {
border-color: #e94560;
background: rgba(233, 69, 96, 0.08);
}
.env-manage-name {
font-size: 13px;
color: #e0e0e0;
display: flex;
align-items: center;
gap: 6px;
}
.env-manage-count {
color: #a0a0b0;
font-size: 11px;
}
.env-manage-active-mark {
display: inline-block;
padding: 1px 6px;
background: #e94560;
color: #fff;
font-size: 10px;
border-radius: 8px;
}
.env-manage-actions {
display: flex;
gap: 6px;
}
.env-manage-actions button {
padding: 4px 10px;
font-size: 12px;
}
.env-manage-empty {
text-align: center;
color: #a0a0b0;
font-size: 12px;
padding: 20px;
}
.strength-block {
margin-top: 10px;
}
+65 -54
View File
@@ -28,23 +28,21 @@
<div class="container" id="mainContainer" style="display:none;">
<header>
<button type="button" id="lockBtn" class="btn-lock" title="锁定(清除内存中的明文)">🔒</button>
<div class="header-title">
<img src="icons/icon128.png" alt="logo" class="header-logo">
<h1>Auto Login Manager</h1>
<button type="button" id="lockBtn" class="btn-lock" title="锁定(清除内存中的明文)">🔒 锁定</button>
</div>
<div class="env-bar">
<label class="env-label">当前环境</label>
<div class="env-controls">
<select id="envSelect" class="env-select"></select>
<button type="button" id="addEnvBtn" class="btn-env" title="新建环境">+</button>
<button type="button" id="renameEnvBtn" class="btn-env" title="重命名当前环境">✎</button>
<button type="button" id="deleteEnvBtn" class="btn-env btn-env-danger" title="删除当前环境">🗑</button>
<button type="button" id="envManageBtn" class="btn-env" title="管理环境(新建 / 重命名 / 删除)">⚙</button>
</div>
</div>
</header>
<section class="add-section">
<section class="add-section" id="addSection">
<h2>添加域名配置</h2>
<div class="form-group">
<label for="domain">域名 / URL 匹配规则</label>
@@ -54,11 +52,6 @@
</div>
<small>支持通配符 *。带 / 时按 hostname+path 匹配,如 oa.com/login/*;不带 / 时只匹配域名</small>
</div>
<div class="form-group">
<label for="alias">别名(可选)</label>
<input type="text" id="alias" placeholder="例如: 公司OA、GitLab、邮箱">
<small>方便识别该配置,留空则显示域名</small>
</div>
<div class="form-group">
<label for="username">用户名</label>
<input type="text" id="username" placeholder="输入用户名">
@@ -70,57 +63,62 @@
<button class="toggle-pwd" type="button" data-target="password">显示</button>
</div>
</div>
<div class="form-row">
<div class="form-group form-group-inline">
<label for="autoSubmit">自动提交登录</label>
<select id="autoSubmit">
<option value="true">是 - 自动点击登录按钮</option>
<option value="false">否 - 仅填充不提交</option>
</select>
</div>
<div class="form-group form-group-inline">
<label for="enabled">启用</label>
<label class="toggle-switch">
<input type="checkbox" id="enabled" checked>
<span class="toggle-slider"></span>
</label>
<small>关闭后该配置不再参与匹配(保留数据)</small>
</div>
</div>
<div class="form-group">
<label for="delayMs">填充延迟(毫秒,可选)</label>
<input type="number" id="delayMs" min="0" max="10000" step="100" placeholder="留空=默认 300ms,可填 500-2000 用于 SPA 页面">
<small>某些页面(Ant Design、ASP.NET)需 JS 初始化,立即填充会失效</small>
<label for="alias">别名(可选)</label>
<input type="text" id="alias" placeholder="例如: 公司OA、GitLab、邮箱">
<small>方便识别该配置,留空则显示域名</small>
</div>
<details class="selector-help">
<summary>高级选项:自定义输入框和登录按钮位置(选填)</summary>
<div class="selector-help-body">
<p class="help-tip">下面三项通常<strong>留空即可</strong>,插件会自动找到登录框。只有当自动填充失败、或填错位置时才需要填。</p>
<p class="help-step">1. 在登录页面按 <kbd>F12</kbd> 打开开发者工具,切到"Elements(元素)"标签</p>
<p class="help-step">2. 点开发者工具左上角的 <span class="icon-arrow">箭头图标</span>,再去点页面上的输入框/按钮</p>
<p class="help-step">3. 工具会跳到对应的 HTML 行,在 <code>&lt;input&gt;</code> 或 <code>&lt;button&gt;</code> 标签上看 <code>id</code>、<code>name</code>、<code>class</code>、<code>type</code> 属性</p>
<p class="help-step">4. 按 <span class="rule-tag">#id</span>、<span class="rule-tag">.class</span>、<span class="rule-tag">[name=xxx]</span>、<span class="rule-tag">tag[属性=值]</span> 规则组合填写即可</p>
<details class="advanced-options">
<summary>高级选项(选填,默认即可)</summary>
<div class="advanced-options-body">
<div class="form-group">
<label for="autoSubmit">自动提交登录</label>
<select id="autoSubmit">
<option value="true">是 - 自动点击登录按钮</option>
<option value="false">否 - 仅填充不提交</option>
</select>
</div>
<div class="form-group">
<label for="enabled">启用 / 禁用</label>
<label class="toggle-switch">
<input type="checkbox" id="enabled" checked>
<span class="toggle-slider"></span>
</label>
<small>禁用后该条配置将不会生效</small>
</div>
<div class="form-group">
<label for="delayMs">填充延迟(毫秒,可选)</label>
<input type="number" id="delayMs" min="0" max="10000" step="100" placeholder="留空=默认 300ms,可填 500-2000 用于 SPA 页面">
<small>某些页面(Ant Design、ASP.NET)需 JS 初始化,立即填充会失效</small>
</div>
<div class="selector-tutorial">
<p class="help-tip">下面三项通常<strong>留空即可</strong>,插件会自动找到登录框。只有当自动填充失败、或填错位置时才需要填。</p>
<p class="help-step">1. 在登录页面按 <kbd>F12</kbd> 打开开发者工具,切到"Elements(元素)"标签</p>
<p class="help-step">2. 点开发者工具左上角的 <span class="icon-arrow">箭头图标</span>,再去点页面上的输入框/按钮</p>
<p class="help-step">3. 工具会跳到对应的 HTML 行,在 <code>&lt;input&gt;</code> 或 <code>&lt;button&gt;</code> 标签上看 <code>id</code>、<code>name</code>、<code>class</code>、<code>type</code> 属性</p>
<p class="help-step">4. 按 <span class="rule-tag">#id</span>、<span class="rule-tag">.class</span>、<span class="rule-tag">[name=xxx]</span>、<span class="rule-tag">tag[属性=值]</span> 规则组合填写即可</p>
</div>
<div class="form-group">
<label>用户名输入框位置</label>
<input type="text" id="usernameSelector" placeholder="留空=自动查找,如 #username 或 input[name=account]">
<small>留空即可。填错会导致用户名填不进框。</small>
</div>
<div class="form-group">
<label>密码输入框位置</label>
<input type="text" id="passwordSelector" placeholder="留空=自动查找,如 #password 或 input[type=password]">
<small>留空即可。填错会导致密码填不进框。</small>
</div>
<div class="form-group">
<label>登录按钮位置</label>
<input type="text" id="submitSelector" placeholder="留空=自动查找,如 #loginBtn 或 button[type=submit]">
<small>留空即可。填错会导致不会自动点击登录。</small>
</div>
</div>
</details>
<div class="form-group">
<label>用户名输入框位置</label>
<input type="text" id="usernameSelector" placeholder="留空=自动查找,如 #username 或 input[name=account]">
<small>留空即可。填错会导致用户名填不进框。</small>
</div>
<div class="form-group">
<label>密码输入框位置</label>
<input type="text" id="passwordSelector" placeholder="留空=自动查找,如 #password 或 input[type=password]">
<small>留空即可。填错会导致密码填不进框。</small>
</div>
<div class="form-group">
<label>登录按钮位置</label>
<input type="text" id="submitSelector" placeholder="留空=自动查找,如 #loginBtn 或 button[type=submit]">
<small>留空即可。填错会导致不会自动点击登录。</small>
</div>
<button id="addBtn" class="btn-primary">添加配置</button>
</section>
<section class="list-section">
<section class="list-section" id="listSection">
<div class="list-header">
<h2>已配置域名</h2>
<div class="list-actions">
@@ -144,6 +142,19 @@
<div id="toast" class="toast"></div>
<!-- 环境管理对话框:列出所有环境,每行带编辑/删除,底部新建按钮 -->
<div id="envManageModal" class="modal">
<div class="modal-content">
<h3>环境管理</h3>
<div id="envManageList" class="env-manage-list"></div>
<div class="modal-warn" id="envManageWarn" style="display:none;"></div>
<div class="modal-actions">
<button type="button" id="envManageClose" class="btn-edit">关闭</button>
<button type="button" id="envManageAdd" class="btn-primary">+ 新建环境</button>
</div>
</div>
</div>
<div id="envModal" class="modal">
<div class="modal-content">
<h3 id="envModalTitle">新建环境</h3>
+163 -48
View File
@@ -13,6 +13,8 @@ document.addEventListener("DOMContentLoaded", () => {
const submitSelectorInput = $("submitSelector");
const addBtn = $("addBtn");
const configList = $("configList");
const addSection = $("addSection");
const listSection = $("listSection");
const fillCurrentBtn = $("fillCurrent");
const fillDomainBtn = $("fillDomain");
const exportBtn = $("exportBtn");
@@ -34,9 +36,12 @@ document.addEventListener("DOMContentLoaded", () => {
// 环境栏
const envSelect = $("envSelect");
const addEnvBtn = $("addEnvBtn");
const renameEnvBtn = $("renameEnvBtn");
const deleteEnvBtn = $("deleteEnvBtn");
const envManageBtn = $("envManageBtn");
const envManageModal = $("envManageModal");
const envManageList = $("envManageList");
const envManageWarn = $("envManageWarn");
const envManageClose = $("envManageClose");
const envManageAdd = $("envManageAdd");
const envModal = $("envModal");
const envModalTitle = $("envModalTitle");
const envNameInput = $("envNameInput");
@@ -474,47 +479,104 @@ document.addEventListener("DOMContentLoaded", () => {
showToast("已切换环境");
});
addEnvBtn.addEventListener("click", async () => {
const name = await openEnvModal("create");
if (!name) return;
await createEnvironment(name);
await renderEnvSelect();
resetForm();
await renderConfigList();
showToast(`已创建环境「${name}」`);
});
// ---------- 环境管理对话框 ----------
function openEnvManageModal() {
envManageWarn.textContent = "";
envManageWarn.style.display = "none";
renderEnvManageList();
envManageModal.classList.add("show");
}
renameEnvBtn.addEventListener("click", async () => {
const env = await getActiveEnv();
if (!env) return;
const name = await openEnvModal("rename", env.name);
if (!name) return;
await renameEnvironment(env.id, name);
await renderEnvSelect();
showToast("已重命名");
});
function closeEnvManageModal() {
envManageModal.classList.remove("show");
}
deleteEnvBtn.addEventListener("click", async () => {
const env = await getActiveEnv();
if (!env) return;
const { environments } = await loadEnvironments();
if (environments.length <= 1) {
showToast("至少保留一个环境");
async function renderEnvManageList() {
const { environments, activeEnvId } = await loadEnvironments();
envManageList.innerHTML = "";
if (environments.length === 0) {
envManageList.innerHTML = '<div class="env-manage-empty">暂无环境</div>';
return;
}
const cnt = env.configs?.length || 0;
const msg = cnt > 0
? `环境「${env.name}」下有 ${cnt} 条配置,确认删除?(将一并删除这些配置)`
: `确认删除环境「${env.name}」?`;
if (!confirm(msg)) 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">当前</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-edit";
editBtn.textContent = "重命名";
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("已重命名");
} catch (err) {
showToast(err.message || "重命名失败");
}
});
const delBtn = document.createElement("button");
delBtn.className = "btn-danger";
delBtn.textContent = "删除";
delBtn.addEventListener("click", async () => {
if (environments.length <= 1) {
showToast("至少保留一个环境");
return;
}
const cnt2 = env.configs?.length || 0;
const msg = cnt2 > 0
? `环境「${env.name}」下有 ${cnt2} 条配置,确认删除?(将一并删除这些配置)`
: `确认删除环境「${env.name}」?`;
if (!confirm(msg)) return;
try {
await deleteEnvironment(env.id);
await renderEnvManageList();
await renderEnvSelect();
resetForm();
await renderConfigList();
showToast("已删除环境");
} catch (err) {
showToast(err.message || "删除失败");
}
});
actions.appendChild(editBtn);
actions.appendChild(delBtn);
row.appendChild(nameSpan);
row.appendChild(actions);
envManageList.appendChild(row);
});
}
envManageBtn.addEventListener("click", openEnvManageModal);
envManageClose.addEventListener("click", closeEnvManageModal);
envManageAdd.addEventListener("click", async () => {
const name = await openEnvModal("create");
if (!name) return;
try {
await deleteEnvironment(env.id);
await createEnvironment(name);
await renderEnvManageList();
await renderEnvSelect();
resetForm();
await renderConfigList();
showToast("已删除环境");
showToast(`已创建环境「${name}」`);
} catch (err) {
showToast(err.message || "删除失败");
showToast(err.message || "创建失败");
}
});
@@ -523,8 +585,34 @@ document.addEventListener("DOMContentLoaded", () => {
const configs = await loadConfigs();
configList.innerHTML = "";
if (configs.length === 0) {
configList.innerHTML = '<div class="empty-msg">暂无配置,请在上方添加</div>';
// 配置为 0:禁用导出,列表在前;非 0:启用导出,添加表单在前
const isEmpty = configs.length === 0;
exportBtn.disabled = isEmpty;
exportBtn.classList.toggle("btn-disabled", isEmpty);
const parent = addSection.parentElement;
if (isEmpty) {
// 空状态:add-section 在上,list-section 在下(已是默认顺序,确保如此)
if (parent.children[0] !== addSection) parent.insertBefore(addSection, listSection);
} else {
// 非空:list-section 在上,add-section 在下
if (parent.children[0] !== listSection) 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">暂无配置</div>
<div class="empty-sub">在上方填写域名 / 用户名 / 密码后点击「添加配置」</div>
</div>
`;
return;
}
@@ -562,6 +650,8 @@ document.addEventListener("DOMContentLoaded", () => {
);
await saveConfigs(updated);
renderConfigList();
renderEnvSelect();
if (envManageModal.classList.contains("show")) renderEnvManageList();
showToast(cfg.enabled === false ? "已启用" : "已禁用");
});
@@ -593,6 +683,8 @@ document.addEventListener("DOMContentLoaded", () => {
await saveConfigs(updated);
showToast("已删除");
renderConfigList();
renderEnvSelect();
if (envManageModal.classList.contains("show")) renderEnvManageList();
if (editingId === cfg.id) {
resetForm();
}
@@ -670,6 +762,8 @@ document.addEventListener("DOMContentLoaded", () => {
await saveConfigs(configs);
resetForm();
renderConfigList();
renderEnvSelect();
if (envManageModal.classList.contains("show")) renderEnvManageList();
});
fillCurrentBtn.addEventListener("click", async () => {
@@ -689,21 +783,42 @@ document.addEventListener("DOMContentLoaded", () => {
}
if (matched.length === 1) {
chrome.tabs.sendMessage(tab.id, {
action: "manualFill",
config: matched[0],
});
showToast("已发送填充指令");
try {
const resp = await chrome.tabs.sendMessage(tab.id, {
action: "manualFill",
config: matched[0],
});
if (resp && resp.success) {
showToast("已填充,请确认");
} else {
showToast("已发送但未找到登录表单,请确认页面已加载完成");
}
} catch (err) {
// content script 未注入:常见于安装扩展前已打开的页面
if (/Could not establish connection|Receiving end does not exist/i.test(err.message)) {
showToast("页面未注入脚本,请刷新页面后重试");
} else {
showToast("发送失败:" + (err.message || err));
}
}
return;
}
// 多个环境命中:发到 content.js 由它弹选择菜单
chrome.tabs.sendMessage(tab.id, {
action: "manualFillMulti",
configs: matched,
tabUrl: tab.url,
});
showToast(`命中 ${matched.length} 个环境配置,请在页面选择`);
try {
await chrome.tabs.sendMessage(tab.id, {
action: "manualFillMulti",
configs: matched,
tabUrl: tab.url,
});
showToast(`命中 ${matched.length} 个环境配置,请在页面选择`);
} catch (err) {
if (/Could not establish connection|Receiving end does not exist/i.test(err.message)) {
showToast("页面未注入脚本,请刷新页面后重试");
} else {
showToast("发送失败:" + (err.message || err));
}
}
});
fillDomainBtn.addEventListener("click", async () => {
+12
View File
@@ -118,6 +118,18 @@ describe("env-store - 环境 CRUD", () => {
const env = await EnvStore.getActiveEnv();
assert.equal(env.id, "e3");
});
it("超过 5 个环境时抛错", async () => {
// 已有 3 个,再加 2 个到上限
await EnvStore.createEnvironment("env4");
await EnvStore.createEnvironment("env5");
const { environments } = await EnvStore.loadEnvironments();
assert.equal(environments.length, 5);
// 第 6 个应被拒绝
await assert.rejects(() => EnvStore.createEnvironment("env6"), /最多 5 个环境/);
const after = await EnvStore.loadEnvironments();
assert.equal(after.environments.length, 5);
});
});
describe("env-store - 配置 CRUD", () => {