docs: define compact proxy editor task
This commit is contained in:
@@ -122,9 +122,9 @@
|
||||
.status.associated { color: var(--warning); background: var(--warning-soft); }
|
||||
.status.occupied { color: var(--warning); background: var(--warning-soft); }
|
||||
.status.exited { color: var(--muted); background: var(--surface-3); }
|
||||
.row-actions { display: flex; justify-content: flex-end; gap: 6px; opacity: .7; }
|
||||
.row-actions { display: flex; justify-content: flex-end; gap: 10px; opacity: .7; }
|
||||
tr:hover .row-actions, tr.selected .row-actions { opacity: 1; }
|
||||
.row-actions button { background: transparent; color: var(--muted); padding: 6px 8px; border-radius: 6px; }
|
||||
.row-actions button { background: transparent; color: var(--muted); padding: 8px 10px; border-radius: 6px; }
|
||||
.row-actions button:hover { color: var(--accent-strong); background: var(--accent-soft); }
|
||||
.table-footer { display: flex; align-items: center; justify-content: space-between; padding: 12px 16px; color: var(--muted); font-size: 12px; }
|
||||
.drawer-backdrop { position: fixed; inset: 0; background: rgba(16, 24, 40, .28); z-index: 10; opacity: 0; pointer-events: none; transition: opacity .16s ease; }
|
||||
@@ -262,7 +262,7 @@
|
||||
<div class="field"><label for="defaultDataDir">默认 user data dir 根目录</label><div class="path-field"><input id="defaultDataDir" value="D:\\Chub\\profiles"><button type="button" class="secondary browse-setting" data-target="defaultDataDir">选择目录</button><button type="button" class="secondary search-setting" data-target="defaultDataDir">搜索</button></div><small>新建实例时作为默认建议路径;不会移动或删除已有 profile。</small></div>
|
||||
<div class="field"><label for="logDir">日志目录</label><div class="path-field"><input id="logDir" value="D:\\Chub\\logs"><button type="button" class="secondary browse-setting" data-target="logDir">选择目录</button><button type="button" class="secondary search-setting" data-target="logDir">搜索</button></div><small>日志仅保存必要诊断信息,并过滤敏感参数。</small></div>
|
||||
</div>
|
||||
<div class="form-section"><h3>代理</h3><p class="help">仅保存无认证代理;名称可在实例表单中选择。</p><div class="field"><label for="proxyName">代理名称</label><input id="proxyName" placeholder="例如:新加坡出口"></div><div class="field"><label for="proxyServer">代理地址</label><input id="proxyServer" placeholder="例如:http://127.0.0.1:8080"><small>支持 http、https、socks4、socks5 的 <span class="mono">scheme://host:port</span>;不允许用户名、密码或路径。</small></div><div class="dialog-actions"><button type="button" class="secondary" id="proxyCancel" hidden>取消编辑</button><button type="button" class="secondary" id="proxySave">添加代理</button></div><div class="setting-list" id="proxyList" aria-label="已保存代理"></div></div>
|
||||
<div class="form-section"><h3>代理</h3><p class="help">仅保存无认证代理。下拉框、实例创建和编辑均展示完整地址。</p><div class="field"><label for="proxySelect">已保存代理</label><select id="proxySelect"><option value="">选择代理(新建)</option></select><small>选择后会回填地址;选择“新建”会清空输入框。</small></div><div class="field"><label for="proxyServer">代理地址</label><div class="path-field"><input id="proxyServer" placeholder="例如:http://127.0.0.1:8080"><button type="button" class="primary" id="proxySave">保存代理</button><button type="button" class="danger-button" id="proxyDelete" disabled>删除代理</button></div><small>支持 http、https、socks4、socks5 的 <span class="mono">scheme://host:port</span>;不允许用户名、密码或路径。</small></div><div class="settings-status" id="proxyStatus">新建代理:填写地址后保存。</div></div>
|
||||
<div class="settings-actions"><button type="button" class="secondary" id="discardSettings">取消</button><button type="submit" class="primary">保存设置</button></div>
|
||||
</form>
|
||||
<aside class="settings-card"><div><h2>运行行为</h2><p>影响后续启动和应用关闭时的默认行为。</p></div><div class="setting-list"><label class="check-line"><input type="checkbox" id="closeOnExit"><span><b>退出 Chub 时关闭本工具启动的实例</b><br><small class="help">默认关闭。不会影响外部浏览器。</small></span></label><div class="field"><label for="remoteDebugPort">远程调试起始端口</label><input id="remoteDebugPort" type="number" min="1024" max="65535" value="9666" inputmode="numeric"><small>留空时使用 9666。启动实例仅绑定 127.0.0.1;端口被占用时顺序尝试下一个可用端口。</small></div><div class="settings-status" id="settingsStatus">设置已保存 · 最后更新:刚刚</div></div></aside>
|
||||
@@ -276,6 +276,7 @@
|
||||
<dialog id="confirmDialog" aria-labelledby="dialogTitle"><div class="dialog-inner"><h2 id="dialogTitle">关闭浏览器实例?</h2><p id="dialogText">将请求浏览器正常退出。未保存的网页内容由浏览器自行处理。</p><div class="dialog-warning" id="dialogWarning" hidden>强制终止可能造成 profile 数据损坏。仅在浏览器无响应时使用。</div><div class="dialog-actions"><button class="secondary" id="dialogCancel">取消</button><button class="primary" id="dialogConfirm">优雅关闭</button></div></div></dialog>
|
||||
<dialog class="edit-dialog" id="editDialog" aria-labelledby="editTitle"><form class="dialog-inner edit-form" id="editForm"><div><h2 id="editTitle">编辑实例</h2><p id="editSubtitle">修改下次启动使用的保存配置。</p></div><div class="field"><label for="editName">实例名称 <span>*</span></label><input id="editName" required><small>用于在实例列表中识别此浏览器环境。</small></div><div class="field"><span>浏览器类型</span><div class="radio-row" role="radiogroup" aria-label="浏览器类型"><label><input type="radio" name="editKind" value="chrome" id="editChrome"> Chrome</label><label><input type="radio" name="editKind" value="edge" id="editEdge"> Edge</label></div></div><div class="field"><label for="editDataDir">User Data Dir <span>*</span></label><div class="path-field"><input id="editDataDir" required><button type="button" class="secondary" id="chooseEditDir">选择目录</button></div><small>必须是绝对路径;同一目录不能被多个实例同时使用。</small></div><div class="field"><label for="editURL">启动 URL(可选)</label><input id="editURL" type="url" placeholder="https://example.com"><small>仅支持 http/https;留空时启动浏览器默认页。</small></div><div class="field"><label for="editProxy">代理</label><select id="editProxy"><option value="">无代理</option></select><small>选择已保存的无认证代理;修改后下次启动生效。</small></div><div class="field"><label for="editPort">当前调试端口</label><input class="readonly-value" id="editPort" readonly><small>实际端口由全局起始端口在启动时分配,不能在此固定。</small></div><div class="form-note" id="editReadOnlyNote" hidden>该实例正在运行或为外部关联。为避免配置与运行身份不一致,浏览器类型和 User Data Dir 已锁定。</div><div class="dialog-actions"><button type="button" class="secondary" id="editCancel">取消</button><button type="submit" class="primary" id="editSave">保存更改</button></div></form></dialog>
|
||||
<dialog id="discardDialog" aria-labelledby="discardTitle"><div class="dialog-inner"><h2 id="discardTitle">放弃未保存的更改?</h2><p>可以先保存配置,或放弃本次编辑并关闭对话框。</p><div class="dialog-actions"><button class="secondary" id="keepEditing">继续编辑</button><button class="secondary" id="discardChanges">不保存</button><button class="primary" id="saveChanges">保存</button></div></div></dialog>
|
||||
<dialog id="proxyDeleteDialog" aria-labelledby="proxyDeleteTitle"><div class="dialog-inner"><h2 id="proxyDeleteTitle">删除代理?</h2><p id="proxyDeleteText"></p><div class="dialog-actions"><button class="secondary" id="proxyDeleteCancel">取消</button><button class="danger-button" id="proxyDeleteConfirm">删除代理</button></div></div></dialog>
|
||||
<div class="toast" id="toast" role="status" aria-live="polite"></div>
|
||||
|
||||
<script>
|
||||
@@ -293,12 +294,12 @@
|
||||
let modalAction = 'stop';
|
||||
let editingID = '';
|
||||
let editSnapshot = null;
|
||||
let editingProxyID = '';
|
||||
let selectedProxyID = '';
|
||||
const $ = (id) => document.getElementById(id);
|
||||
const statusClass = (s) => s;
|
||||
const proxyLabel = (id) => proxies.find(p => p.id === id)?.name || '无代理';
|
||||
function renderProxyOptions(select, selected = '') { select.innerHTML = '<option value="">无代理</option>' + proxies.map(p => `<option value="${p.id}">${p.name} · ${p.server}</option>`).join(''); select.value = selected; }
|
||||
function renderProxies() { const list = $('proxyList'); list.innerHTML = proxies.map(p => { const uses = instances.filter(i => i.proxyId === p.id).length; return `<div class="detail-line"><span><b>${p.name}</b><br><small class="mono">${p.server}</small></span><span><small>${uses ? `已被 ${uses} 个实例使用` : '未使用'}</small> <button type="button" class="secondary" data-proxy-edit="${p.id}">编辑</button><button type="button" class="secondary" data-proxy-delete="${p.id}">删除</button></span></div>`; }).join('') || '<small class="help">尚无代理。实例将以无代理方式启动。</small>'; list.querySelectorAll('[data-proxy-edit]').forEach(b => b.onclick = () => { const p = proxies.find(item => item.id === b.dataset.proxyEdit); editingProxyID = p.id; $('proxyName').value = p.name; $('proxyServer').value = p.server; $('proxySave').textContent = '保存代理'; $('proxyCancel').hidden = false; $('proxyName').focus(); }); list.querySelectorAll('[data-proxy-delete]').forEach(b => b.onclick = () => { const p = proxies.find(item => item.id === b.dataset.proxyDelete), uses = instances.filter(i => i.proxyId === p.id).length; if (uses) return showToast(`“${p.name}”仍被 ${uses} 个实例使用,不能删除。`); proxies.splice(proxies.indexOf(p), 1); renderProxies(); showToast(`已删除代理“${p.name}”。`); }); }
|
||||
const proxyLabel = (id) => proxies.find(p => p.id === id)?.server || '无代理';
|
||||
function renderProxyOptions(select, selected = '') { select.innerHTML = '<option value="">无代理</option>' + proxies.map(p => `<option value="${p.id}">${p.server}</option>`).join(''); select.value = selected; }
|
||||
function renderProxies() { const select = $('proxySelect'), input = $('proxyServer'), status = $('proxyStatus'), remove = $('proxyDelete'); select.innerHTML = '<option value="">选择代理(新建)</option>' + proxies.map(p => `<option value="${p.id}">${p.server}</option>`).join(''); if (!proxies.some(p => p.id === selectedProxyID)) selectedProxyID = ''; select.value = selectedProxyID; const selected = proxies.find(p => p.id === selectedProxyID); input.value = selected?.server || ''; remove.disabled = !selected; status.textContent = selected ? `正在编辑已保存代理;已被 ${instances.filter(i => i.proxyId === selected.id).length} 个实例使用。` : '新建代理:填写地址后保存。'; }
|
||||
function filtered() {
|
||||
const q = $('search').value.trim().toLowerCase(), browser = $('browserFilter').value, status = $('statusFilter').value;
|
||||
return instances.filter(x => (!q || [x.name, x.browser, x.pid, x.port, x.dir].join(' ').toLowerCase().includes(q)) && (browser === 'all' || x.kind === browser) && (status === 'all' || x.status === status));
|
||||
@@ -312,7 +313,7 @@
|
||||
rows.forEach(x => { const tr = document.createElement('tr'); tr.className = x.id === selectedId ? 'selected' : ''; tr.tabIndex = 0; tr.dataset.id = x.id; tr.title = '双击或按 Enter 编辑实例'; tr.innerHTML = `
|
||||
<td><div class="instance-name"><span class="browser-logo ${x.kind}" aria-hidden="true">${x.kind === 'chrome' ? 'C' : 'E'}</span><div><strong>${x.name}</strong><small>${x.id}</small></div></div></td>
|
||||
<td>${x.browser}</td><td class="mono">${x.dir}</td><td class="mono">${x.port || '—'}</td><td><span class="status ${statusClass(x.status)}">${x.statusText}</span></td>
|
||||
<td><div class="row-actions"><button data-action="edit" aria-label="编辑 ${x.name}">编辑</button><button data-action="start" aria-label="${x.status === 'associated' ? '重新检测' : x.status === 'running' ? '停止' : '启动'} ${x.name}">${x.status === 'associated' ? '重新检测' : x.status === 'running' ? '■ 停止' : '▶ 启动'}</button><button data-action="delete" aria-label="删除 ${x.name}">删除</button></div></td>`;
|
||||
<td><div class="row-actions"><button data-action="start" aria-label="${x.status === 'associated' ? '重新检测' : x.status === 'running' ? '停止' : '启动'} ${x.name}">${x.status === 'associated' ? '重新检测' : x.status === 'running' ? '■ 停止' : '▶ 启动'}</button><button data-action="edit" aria-label="编辑 ${x.name}">编辑</button><button data-action="delete" aria-label="删除 ${x.name}">删除</button></div></td>`;
|
||||
tr.addEventListener('click', e => { if (!e.target.closest('button')) selectRow(x.id, body); });
|
||||
tr.addEventListener('dblclick', e => { if (!e.target.closest('button')) openEdit(x.id); });
|
||||
tr.addEventListener('keydown', e => { if (e.key === 'Enter') { e.preventDefault(); openEdit(x.id); } else if (e.key === ' ') { e.preventDefault(); selectRow(x.id, body); } });
|
||||
@@ -360,8 +361,10 @@
|
||||
document.querySelectorAll('[data-nav]').forEach(b => b.onclick = () => { document.querySelectorAll('[data-nav]').forEach(n => n.removeAttribute('aria-current')); b.setAttribute('aria-current', 'page'); setPage(b.dataset.nav); });
|
||||
document.querySelectorAll('.browse-setting').forEach(b => b.onclick = () => showToast(`原型演示:将打开${b.dataset.target.includes('Path') ? '文件' : '目录'}选择器。`));
|
||||
document.querySelectorAll('.search-setting').forEach(b => b.onclick = () => { if (b.dataset.timer) { clearTimeout(Number(b.dataset.timer)); delete b.dataset.timer; b.textContent = '搜索'; showToast('已取消当前搜索。'); return; } b.textContent = '取消'; showToast('正在搜索可用路径…'); b.dataset.timer = String(setTimeout(() => { delete b.dataset.timer; b.textContent = '搜索'; showToast('搜索完成:已更新当前字段。'); }, 1600)); });
|
||||
$('proxySave').onclick = () => { const name = $('proxyName').value.trim(), server = $('proxyServer').value.trim(); if (!name || !server || !/^(https?|socks[45]):\/\/[^\/@?#:]+(?::\d+)$/.test(server)) { showToast('请填写名称和无认证的 scheme://host:port 代理地址。'); return; } if (proxies.some(p => p.name === name && p.id !== editingProxyID)) { showToast('代理名称已存在,请使用不同名称。'); return; } if (editingProxyID) { const p = proxies.find(item => item.id === editingProxyID); p.name = name; p.server = server; } else proxies.push({ id: `proxy-${Date.now()}`, name, server }); editingProxyID = ''; $('proxyName').value = ''; $('proxyServer').value = ''; $('proxySave').textContent = '添加代理'; $('proxyCancel').hidden = true; renderProxies(); showToast('代理已保存;后续启动将使用最新端点。'); };
|
||||
$('proxyCancel').onclick = () => { editingProxyID = ''; $('proxyName').value = ''; $('proxyServer').value = ''; $('proxySave').textContent = '添加代理'; $('proxyCancel').hidden = true; };
|
||||
$('proxySelect').onchange = () => { selectedProxyID = $('proxySelect').value; renderProxies(); $('proxyServer').focus(); };
|
||||
$('proxySave').onclick = () => { const server = $('proxyServer').value.trim().toLowerCase(); if (!/^(https?|socks[45]):\/\/[^\/@?#:]+(?::\d+)$/.test(server)) { $('proxyStatus').textContent = '请填写无认证的 scheme://host:port 代理地址。'; return; } if (proxies.some(p => p.server === server && p.id !== selectedProxyID)) { $('proxyStatus').textContent = '该代理地址已存在,请从下拉框选择它。'; return; } if (selectedProxyID) { const p = proxies.find(item => item.id === selectedProxyID); p.server = server; p.name = server; } else { selectedProxyID = `proxy-${Date.now()}`; proxies.push({ id: selectedProxyID, name: server, server }); } renderProxies(); showToast('代理已保存;后续启动将使用最新端点。'); };
|
||||
$('proxyDelete').onclick = () => { const proxy = proxies.find(p => p.id === selectedProxyID), uses = instances.filter(i => i.proxyId === selectedProxyID).length; if (!proxy) return; if (uses) { $('proxyStatus').textContent = `该代理仍被 ${uses} 个实例使用,不能删除。`; return; } $('proxyDeleteText').textContent = `确定删除代理“${proxy.server}”?此操作不会修改任何实例。`; $('proxyDeleteDialog').showModal(); setTimeout(() => $('proxyDeleteCancel').focus(), 0); };
|
||||
$('proxyDeleteCancel').onclick = () => $('proxyDeleteDialog').close(); $('proxyDeleteDialog').addEventListener('cancel', e => { e.preventDefault(); $('proxyDeleteDialog').close(); }); $('proxyDeleteConfirm').onclick = () => { const proxy = proxies.find(p => p.id === selectedProxyID); if (proxy) proxies.splice(proxies.indexOf(proxy), 1); selectedProxyID = ''; $('proxyDeleteDialog').close(); renderProxies(); $('proxySelect').focus(); showToast('代理已删除。'); };
|
||||
$('settingsForm').addEventListener('submit', e => { e.preventDefault(); $('settingsStatus').textContent = '设置已保存 · 最后更新:刚刚'; $('settingsStatus').classList.remove('warning'); showToast('设置已保存,后续新建实例将使用新的默认值。'); });
|
||||
$('discardSettings').onclick = () => { $('settingsForm').reset(); $('settingsStatus').textContent = '已取消本次修改,恢复到上次保存值。'; $('settingsStatus').classList.add('warning'); showToast('已取消设置修改。'); };
|
||||
renderRows(); renderProxies();
|
||||
|
||||
Reference in New Issue
Block a user