no message
This commit is contained in:
@@ -63,8 +63,13 @@
|
|||||||
await storage().set({ [ACTIVE_ENV_KEY]: envId });
|
await storage().set({ [ACTIVE_ENV_KEY]: envId });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const MAX_ENVIRONMENTS = 5;
|
||||||
|
|
||||||
async function createEnvironment(name) {
|
async function createEnvironment(name) {
|
||||||
const { environments } = await loadEnvironments();
|
const { environments } = await loadEnvironments();
|
||||||
|
if (environments.length >= MAX_ENVIRONMENTS) {
|
||||||
|
throw new Error(`最多 ${MAX_ENVIRONMENTS} 个环境`);
|
||||||
|
}
|
||||||
const env = { id: genId(), name, configs: [] };
|
const env = { id: genId(), name, configs: [] };
|
||||||
environments.push(env);
|
environments.push(env);
|
||||||
await saveEnvironments(environments, env.id);
|
await saveEnvironments(environments, env.id);
|
||||||
@@ -133,6 +138,7 @@
|
|||||||
return {
|
return {
|
||||||
ENV_KEY,
|
ENV_KEY,
|
||||||
ACTIVE_ENV_KEY,
|
ACTIVE_ENV_KEY,
|
||||||
|
MAX_ENVIRONMENTS,
|
||||||
loadEnvironments,
|
loadEnvironments,
|
||||||
saveEnvironments,
|
saveEnvironments,
|
||||||
getActiveEnv,
|
getActiveEnv,
|
||||||
|
|||||||
@@ -386,6 +386,13 @@ h2 {
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.btn-lock {
|
||||||
|
position: fixed;
|
||||||
|
top: 8px;
|
||||||
|
right: 8px;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
.form-group label {
|
.form-group label {
|
||||||
display: block;
|
display: block;
|
||||||
margin-bottom: 4px;
|
margin-bottom: 4px;
|
||||||
@@ -422,57 +429,59 @@ h2 {
|
|||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.selector-help {
|
.advanced-options {
|
||||||
background: #16213e;
|
background: #16213e;
|
||||||
border: 1px solid #0f3460;
|
border: 1px solid #0f3460;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
padding: 8px 10px;
|
padding: 10px 12px;
|
||||||
margin-bottom: 12px;
|
margin-bottom: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.selector-help summary {
|
.advanced-options summary {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
color: #a0a0b0;
|
color: #a0a0b0;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
list-style: none;
|
list-style: none;
|
||||||
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
.selector-help summary::-webkit-details-marker {
|
.advanced-options summary::-webkit-details-marker {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.selector-help summary::before {
|
.advanced-options summary::before {
|
||||||
content: "▶ ";
|
content: "▶";
|
||||||
color: #e94560;
|
color: #e94560;
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
|
margin-right: 10px;
|
||||||
transition: transform 0.2s;
|
transition: transform 0.2s;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
}
|
}
|
||||||
|
|
||||||
.selector-help[open] summary::before {
|
.advanced-options[open] summary::before {
|
||||||
content: "▼ ";
|
content: "▼";
|
||||||
}
|
}
|
||||||
|
|
||||||
.selector-help-body {
|
.advanced-options-body {
|
||||||
margin-top: 10px;
|
margin-top: 12px;
|
||||||
padding-top: 8px;
|
padding-top: 10px;
|
||||||
border-top: 1px dashed #0f3460;
|
border-top: 1px dashed #0f3460;
|
||||||
}
|
}
|
||||||
|
|
||||||
.selector-help-body p {
|
.advanced-options-body p {
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
line-height: 1.6;
|
line-height: 1.6;
|
||||||
color: #a0a0b0;
|
color: #a0a0b0;
|
||||||
margin-bottom: 6px;
|
margin-bottom: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.selector-help-body .help-tip {
|
.advanced-options-body .help-tip {
|
||||||
color: #e94560;
|
color: #e94560;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
.selector-help-body code {
|
.advanced-options-body code {
|
||||||
background: #0f1d3a;
|
background: #0f1d3a;
|
||||||
padding: 1px 4px;
|
padding: 1px 4px;
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
@@ -481,7 +490,7 @@ h2 {
|
|||||||
font-family: Consolas, Monaco, monospace;
|
font-family: Consolas, Monaco, monospace;
|
||||||
}
|
}
|
||||||
|
|
||||||
.selector-help-body kbd {
|
.advanced-options-body kbd {
|
||||||
background: #0f1d3a;
|
background: #0f1d3a;
|
||||||
border: 1px solid #0f3460;
|
border: 1px solid #0f3460;
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
@@ -491,7 +500,7 @@ h2 {
|
|||||||
font-family: Consolas, Monaco, monospace;
|
font-family: Consolas, Monaco, monospace;
|
||||||
}
|
}
|
||||||
|
|
||||||
.selector-help-body .icon-arrow {
|
.advanced-options-body .icon-arrow {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
padding: 1px 5px;
|
padding: 1px 5px;
|
||||||
background: #0f3460;
|
background: #0f3460;
|
||||||
@@ -500,7 +509,7 @@ h2 {
|
|||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.selector-help-body .rule-tag {
|
.advanced-options-body .rule-tag {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
padding: 1px 5px;
|
padding: 1px 5px;
|
||||||
margin: 0 2px;
|
margin: 0 2px;
|
||||||
@@ -512,6 +521,14 @@ h2 {
|
|||||||
font-family: Consolas, Monaco, monospace;
|
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 {
|
.password-wrapper {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
@@ -697,6 +714,47 @@ h2 {
|
|||||||
font-size: 13px;
|
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 {
|
.toast {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
bottom: 10px;
|
bottom: 10px;
|
||||||
@@ -810,6 +868,70 @@ h2 {
|
|||||||
flex: 1;
|
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 {
|
.strength-block {
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
}
|
}
|
||||||
|
|||||||
+65
-54
@@ -28,23 +28,21 @@
|
|||||||
|
|
||||||
<div class="container" id="mainContainer" style="display:none;">
|
<div class="container" id="mainContainer" style="display:none;">
|
||||||
<header>
|
<header>
|
||||||
|
<button type="button" id="lockBtn" class="btn-lock" title="锁定(清除内存中的明文)">🔒</button>
|
||||||
<div class="header-title">
|
<div class="header-title">
|
||||||
<img src="icons/icon128.png" alt="logo" class="header-logo">
|
<img src="icons/icon128.png" alt="logo" class="header-logo">
|
||||||
<h1>Auto Login Manager</h1>
|
<h1>Auto Login Manager</h1>
|
||||||
<button type="button" id="lockBtn" class="btn-lock" title="锁定(清除内存中的明文)">🔒 锁定</button>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="env-bar">
|
<div class="env-bar">
|
||||||
<label class="env-label">当前环境</label>
|
<label class="env-label">当前环境</label>
|
||||||
<div class="env-controls">
|
<div class="env-controls">
|
||||||
<select id="envSelect" class="env-select"></select>
|
<select id="envSelect" class="env-select"></select>
|
||||||
<button type="button" id="addEnvBtn" class="btn-env" title="新建环境">+</button>
|
<button type="button" id="envManageBtn" 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>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<section class="add-section">
|
<section class="add-section" id="addSection">
|
||||||
<h2>添加域名配置</h2>
|
<h2>添加域名配置</h2>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="domain">域名 / URL 匹配规则</label>
|
<label for="domain">域名 / URL 匹配规则</label>
|
||||||
@@ -54,11 +52,6 @@
|
|||||||
</div>
|
</div>
|
||||||
<small>支持通配符 *。带 / 时按 hostname+path 匹配,如 oa.com/login/*;不带 / 时只匹配域名</small>
|
<small>支持通配符 *。带 / 时按 hostname+path 匹配,如 oa.com/login/*;不带 / 时只匹配域名</small>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
|
||||||
<label for="alias">别名(可选)</label>
|
|
||||||
<input type="text" id="alias" placeholder="例如: 公司OA、GitLab、邮箱">
|
|
||||||
<small>方便识别该配置,留空则显示域名</small>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="username">用户名</label>
|
<label for="username">用户名</label>
|
||||||
<input type="text" id="username" placeholder="输入用户名">
|
<input type="text" id="username" placeholder="输入用户名">
|
||||||
@@ -70,57 +63,62 @@
|
|||||||
<button class="toggle-pwd" type="button" data-target="password">显示</button>
|
<button class="toggle-pwd" type="button" data-target="password">显示</button>
|
||||||
</div>
|
</div>
|
||||||
</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">
|
<div class="form-group">
|
||||||
<label for="delayMs">填充延迟(毫秒,可选)</label>
|
<label for="alias">别名(可选)</label>
|
||||||
<input type="number" id="delayMs" min="0" max="10000" step="100" placeholder="留空=默认 300ms,可填 500-2000 用于 SPA 页面">
|
<input type="text" id="alias" placeholder="例如: 公司OA、GitLab、邮箱">
|
||||||
<small>某些页面(Ant Design、ASP.NET)需 JS 初始化,立即填充会失效</small>
|
<small>方便识别该配置,留空则显示域名</small>
|
||||||
</div>
|
</div>
|
||||||
<details class="selector-help">
|
<details class="advanced-options">
|
||||||
<summary>高级选项:自定义输入框和登录按钮位置(选填)</summary>
|
<summary>高级选项(选填,默认即可)</summary>
|
||||||
<div class="selector-help-body">
|
<div class="advanced-options-body">
|
||||||
<p class="help-tip">下面三项通常<strong>留空即可</strong>,插件会自动找到登录框。只有当自动填充失败、或填错位置时才需要填。</p>
|
<div class="form-group">
|
||||||
<p class="help-step">1. 在登录页面按 <kbd>F12</kbd> 打开开发者工具,切到"Elements(元素)"标签</p>
|
<label for="autoSubmit">自动提交登录</label>
|
||||||
<p class="help-step">2. 点开发者工具左上角的 <span class="icon-arrow">箭头图标</span>,再去点页面上的输入框/按钮</p>
|
<select id="autoSubmit">
|
||||||
<p class="help-step">3. 工具会跳到对应的 HTML 行,在 <code><input></code> 或 <code><button></code> 标签上看 <code>id</code>、<code>name</code>、<code>class</code>、<code>type</code> 属性</p>
|
<option value="true">是 - 自动点击登录按钮</option>
|
||||||
<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>
|
<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><input></code> 或 <code><button></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>
|
</div>
|
||||||
</details>
|
</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>
|
<button id="addBtn" class="btn-primary">添加配置</button>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="list-section">
|
<section class="list-section" id="listSection">
|
||||||
<div class="list-header">
|
<div class="list-header">
|
||||||
<h2>已配置域名</h2>
|
<h2>已配置域名</h2>
|
||||||
<div class="list-actions">
|
<div class="list-actions">
|
||||||
@@ -144,6 +142,19 @@
|
|||||||
|
|
||||||
<div id="toast" class="toast"></div>
|
<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 id="envModal" class="modal">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<h3 id="envModalTitle">新建环境</h3>
|
<h3 id="envModalTitle">新建环境</h3>
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
const submitSelectorInput = $("submitSelector");
|
const submitSelectorInput = $("submitSelector");
|
||||||
const addBtn = $("addBtn");
|
const addBtn = $("addBtn");
|
||||||
const configList = $("configList");
|
const configList = $("configList");
|
||||||
|
const addSection = $("addSection");
|
||||||
|
const listSection = $("listSection");
|
||||||
const fillCurrentBtn = $("fillCurrent");
|
const fillCurrentBtn = $("fillCurrent");
|
||||||
const fillDomainBtn = $("fillDomain");
|
const fillDomainBtn = $("fillDomain");
|
||||||
const exportBtn = $("exportBtn");
|
const exportBtn = $("exportBtn");
|
||||||
@@ -34,9 +36,12 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
|
|
||||||
// 环境栏
|
// 环境栏
|
||||||
const envSelect = $("envSelect");
|
const envSelect = $("envSelect");
|
||||||
const addEnvBtn = $("addEnvBtn");
|
const envManageBtn = $("envManageBtn");
|
||||||
const renameEnvBtn = $("renameEnvBtn");
|
const envManageModal = $("envManageModal");
|
||||||
const deleteEnvBtn = $("deleteEnvBtn");
|
const envManageList = $("envManageList");
|
||||||
|
const envManageWarn = $("envManageWarn");
|
||||||
|
const envManageClose = $("envManageClose");
|
||||||
|
const envManageAdd = $("envManageAdd");
|
||||||
const envModal = $("envModal");
|
const envModal = $("envModal");
|
||||||
const envModalTitle = $("envModalTitle");
|
const envModalTitle = $("envModalTitle");
|
||||||
const envNameInput = $("envNameInput");
|
const envNameInput = $("envNameInput");
|
||||||
@@ -474,47 +479,104 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
showToast("已切换环境");
|
showToast("已切换环境");
|
||||||
});
|
});
|
||||||
|
|
||||||
addEnvBtn.addEventListener("click", async () => {
|
// ---------- 环境管理对话框 ----------
|
||||||
const name = await openEnvModal("create");
|
function openEnvManageModal() {
|
||||||
if (!name) return;
|
envManageWarn.textContent = "";
|
||||||
await createEnvironment(name);
|
envManageWarn.style.display = "none";
|
||||||
await renderEnvSelect();
|
renderEnvManageList();
|
||||||
resetForm();
|
envManageModal.classList.add("show");
|
||||||
await renderConfigList();
|
}
|
||||||
showToast(`已创建环境「${name}」`);
|
|
||||||
});
|
|
||||||
|
|
||||||
renameEnvBtn.addEventListener("click", async () => {
|
function closeEnvManageModal() {
|
||||||
const env = await getActiveEnv();
|
envManageModal.classList.remove("show");
|
||||||
if (!env) return;
|
}
|
||||||
const name = await openEnvModal("rename", env.name);
|
|
||||||
if (!name) return;
|
|
||||||
await renameEnvironment(env.id, name);
|
|
||||||
await renderEnvSelect();
|
|
||||||
showToast("已重命名");
|
|
||||||
});
|
|
||||||
|
|
||||||
deleteEnvBtn.addEventListener("click", async () => {
|
async function renderEnvManageList() {
|
||||||
const env = await getActiveEnv();
|
const { environments, activeEnvId } = await loadEnvironments();
|
||||||
if (!env) return;
|
envManageList.innerHTML = "";
|
||||||
const { environments } = await loadEnvironments();
|
|
||||||
if (environments.length <= 1) {
|
if (environments.length === 0) {
|
||||||
showToast("至少保留一个环境");
|
envManageList.innerHTML = '<div class="env-manage-empty">暂无环境</div>';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const cnt = env.configs?.length || 0;
|
|
||||||
const msg = cnt > 0
|
environments.forEach((env) => {
|
||||||
? `环境「${env.name}」下有 ${cnt} 条配置,确认删除?(将一并删除这些配置)`
|
const row = document.createElement("div");
|
||||||
: `确认删除环境「${env.name}」?`;
|
row.className = "env-manage-row" + (env.id === activeEnvId ? " active" : "");
|
||||||
if (!confirm(msg)) return;
|
|
||||||
|
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 {
|
try {
|
||||||
await deleteEnvironment(env.id);
|
await createEnvironment(name);
|
||||||
|
await renderEnvManageList();
|
||||||
await renderEnvSelect();
|
await renderEnvSelect();
|
||||||
resetForm();
|
resetForm();
|
||||||
await renderConfigList();
|
await renderConfigList();
|
||||||
showToast("已删除环境");
|
showToast(`已创建环境「${name}」`);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
showToast(err.message || "删除失败");
|
showToast(err.message || "创建失败");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -523,8 +585,34 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
const configs = await loadConfigs();
|
const configs = await loadConfigs();
|
||||||
configList.innerHTML = "";
|
configList.innerHTML = "";
|
||||||
|
|
||||||
if (configs.length === 0) {
|
// 配置为 0:禁用导出,列表在前;非 0:启用导出,添加表单在前
|
||||||
configList.innerHTML = '<div class="empty-msg">暂无配置,请在上方添加</div>';
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -562,6 +650,8 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
);
|
);
|
||||||
await saveConfigs(updated);
|
await saveConfigs(updated);
|
||||||
renderConfigList();
|
renderConfigList();
|
||||||
|
renderEnvSelect();
|
||||||
|
if (envManageModal.classList.contains("show")) renderEnvManageList();
|
||||||
showToast(cfg.enabled === false ? "已启用" : "已禁用");
|
showToast(cfg.enabled === false ? "已启用" : "已禁用");
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -593,6 +683,8 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
await saveConfigs(updated);
|
await saveConfigs(updated);
|
||||||
showToast("已删除");
|
showToast("已删除");
|
||||||
renderConfigList();
|
renderConfigList();
|
||||||
|
renderEnvSelect();
|
||||||
|
if (envManageModal.classList.contains("show")) renderEnvManageList();
|
||||||
if (editingId === cfg.id) {
|
if (editingId === cfg.id) {
|
||||||
resetForm();
|
resetForm();
|
||||||
}
|
}
|
||||||
@@ -670,6 +762,8 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
await saveConfigs(configs);
|
await saveConfigs(configs);
|
||||||
resetForm();
|
resetForm();
|
||||||
renderConfigList();
|
renderConfigList();
|
||||||
|
renderEnvSelect();
|
||||||
|
if (envManageModal.classList.contains("show")) renderEnvManageList();
|
||||||
});
|
});
|
||||||
|
|
||||||
fillCurrentBtn.addEventListener("click", async () => {
|
fillCurrentBtn.addEventListener("click", async () => {
|
||||||
@@ -689,21 +783,42 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (matched.length === 1) {
|
if (matched.length === 1) {
|
||||||
chrome.tabs.sendMessage(tab.id, {
|
try {
|
||||||
action: "manualFill",
|
const resp = await chrome.tabs.sendMessage(tab.id, {
|
||||||
config: matched[0],
|
action: "manualFill",
|
||||||
});
|
config: matched[0],
|
||||||
showToast("已发送填充指令");
|
});
|
||||||
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 多个环境命中:发到 content.js 由它弹选择菜单
|
// 多个环境命中:发到 content.js 由它弹选择菜单
|
||||||
chrome.tabs.sendMessage(tab.id, {
|
try {
|
||||||
action: "manualFillMulti",
|
await chrome.tabs.sendMessage(tab.id, {
|
||||||
configs: matched,
|
action: "manualFillMulti",
|
||||||
tabUrl: tab.url,
|
configs: matched,
|
||||||
});
|
tabUrl: tab.url,
|
||||||
showToast(`命中 ${matched.length} 个环境配置,请在页面选择`);
|
});
|
||||||
|
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 () => {
|
fillDomainBtn.addEventListener("click", async () => {
|
||||||
|
|||||||
@@ -118,6 +118,18 @@ describe("env-store - 环境 CRUD", () => {
|
|||||||
const env = await EnvStore.getActiveEnv();
|
const env = await EnvStore.getActiveEnv();
|
||||||
assert.equal(env.id, "e3");
|
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", () => {
|
describe("env-store - 配置 CRUD", () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user