docs: define instance data deletion

This commit is contained in:
QiuSW
2026-07-27 22:59:10 +08:00
parent 03b23045a7
commit e400172d04
10 changed files with 75 additions and 3 deletions
+3
View File
@@ -18,6 +18,7 @@
8. 点击“设置”:检查四个路径的 Label、选择/搜索按钮;Chrome/Edge 的“选择”必须打开 Windows 原生 `.exe` 文件选择器,取消后保留输入。搜索期间按钮显示“取消”,再次点击取消当前搜索;搜索命中当前路径时在同一字段旁显示已确认反馈,切换页面后状态保持。
9. 双击“运行中”或“外部已关联”实例,点击“查看标签页”:检查读取中、目标列表、只读边界文案与 Escape/关闭后回到入口。未启动或没有实际端口的实例应禁用入口。
10. 设置代理:检查代理名称和代理地址均有 Label;选择项会回填两项,保存修改后实例列表仅显示最新代理名称。新建实例应默认填入程序目录内不同的 `user_data_dirs\\<实例 ID>` 子目录;窄窗口下代理列不能挤压操作按钮。
11. 点击实例删除:默认确认只删除 Chub 配置。对于已退出实例,勾选“同时删除 User Data Dir 中的数据”后确认按钮改为“删除实例及数据”,并显示目录和不可恢复警告;运行或外部占用实例必须禁用该选项。取消、占用或失败时实例与目录保留。
## 当前原型假设
@@ -27,6 +28,7 @@
- 设置作为独立顶级页面,采用显式提交;取消只恢复本次未保存的表单修改。
- 路径搜索不进行默认全盘扫描:浏览器搜索标准安装路径,目录搜索验证当前输入;Chrome/Edge 路径通过 Windows 原生文件选择器选择既有 `.exe`,新建实例的 User Data Dir 已接入 Windows 原生目录选择器。
- 外部实例显示占用证据,但不提供接管或强制关闭入口。
- 实例删除默认只移除配置;数据删除是明确、默认未选中的附加选择,且只能对已退出/启动失败实例发起。实际删除前仍复核占用和目录边界;原型只模拟结果,不执行文件删除。
- “标签页(CDP 页面目标)”只读取已验证 loopback CDP 端点的展示 DTO;不会显示 WebSocket 调试地址,不提供任何页面或浏览器控制。它不是物理窗口管理视图。
- 原型中的“确认”只模拟事件反馈,不启动真实浏览器或改变文件。
@@ -41,5 +43,6 @@
| 路径选择与搜索 | 选择和搜索均通过后台 adapter、独立请求 ID、取消函数与结果通道;迟到结果按字段/任务 ID 丢弃,反馈紧邻发起字段 |
| 详情抽屉 | 自定义辅助面板,关闭后焦点返回实例行 |
| 确认对话框 | 模态层限制背景交互,Escape 取消,危险操作不默认聚焦 |
| 实例数据删除 | 持久的业务勾选、实例 ID/目录快照/请求代次;后台复核后才允许删除并在成功后持久化配置 |
| CDP 页面目标 | 异步只读 DTO、ID/端口/请求快照防过期、Escape 关闭后返回入口焦点 |
| Toast/状态区 | 非模态状态反馈;异步结果通过统一事件触发重绘 |
+12 -1
View File
@@ -298,6 +298,7 @@
<div class="drawer-backdrop" id="backdrop"></div>
<aside class="drawer" id="drawer" aria-label="实例详情和配置" aria-hidden="true"></aside>
<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 id="instanceDeleteDialog" aria-labelledby="instanceDeleteTitle" aria-describedby="instanceDeleteText"><div class="dialog-inner"><h2 id="instanceDeleteTitle">删除实例?</h2><p id="instanceDeleteText"></p><label class="check-line"><input type="checkbox" id="deleteInstanceData"><span><b>同时删除 User Data Dir 中的数据</b><br><small class="help">默认不删除。目录中的浏览器数据删除后无法恢复。</small></span></label><div class="form-note" id="instanceDeleteUnavailable" hidden></div><div class="dialog-warning" id="instanceDeleteWarning" hidden></div><div class="dialog-actions"><button class="secondary" id="instanceDeleteCancel">取消</button><button class="danger-button" id="instanceDeleteConfirm">删除实例</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="editPreferredPort">首选调试端口</label><input id="editPreferredPort" type="number" min="1024" max="65535" inputmode="numeric"><small>下次启动优先使用;与其他 Chub 实例重复时会提示修改。</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="editTabs">查看标签页</button><button type="button" class="secondary" id="editCancel">取消</button><button type="submit" class="primary" id="editSave">保存更改</button></div></form></dialog>
<dialog class="tabs-dialog" id="tabsDialog" aria-labelledby="tabsTitle" aria-describedby="tabsMeta"><div class="dialog-inner"><div><h2 id="tabsTitle">标签页(CDP 页面目标)</h2><p id="tabsSubtitle">只读查看经验证的本机 CDP 页面目标。</p></div><div class="tabs-meta" id="tabsMeta">不会显示 WebSocket 地址,也不会导航、刷新、关闭页面或执行脚本。</div><div class="tabs-state" id="tabsState" role="status" aria-live="polite">正在读取已验证的本地 CDP 页面目标…</div><div class="tabs-target-list" id="tabsTargetList" hidden></div><div class="dialog-actions"><button class="primary" id="tabsClose">关闭</button></div></div></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>
@@ -324,6 +325,8 @@
let unexpectedExitIDs = [];
let unexpectedExitFocusID = '';
let tabsRequest = 0;
let pendingInstanceDeleteID = '';
let instanceDeleteRunning = false;
const $ = (id) => document.getElementById(id);
const statusClass = (s) => s;
const proxyLabel = (id) => proxies.find(p => p.id === id)?.server || '无代理';
@@ -347,7 +350,7 @@
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); } });
tr.querySelectorAll('button').forEach(b => b.addEventListener('click', e => { e.stopPropagation(); selectRow(x.id, body); if (b.dataset.action === 'edit') openEdit(x.id); else if (b.dataset.action === 'delete') showToast(`原型演示:将确认删除“${x.name}”的 Chub 配置,不会删除 User Data Dir。`); else if (x.status === 'running') { x.status = 'stopping'; x.statusText = '停止中'; renderRows(); setTimeout(() => { x.status = 'exited'; x.statusText = '已退出'; x.pid = '—'; x.port = 0; renderRows(); showToast(`“${x.name}”已优雅退出。`); }, 700); } else openConfirm(x.id, 'start'); }));
tr.querySelectorAll('button').forEach(b => b.addEventListener('click', e => { e.stopPropagation(); selectRow(x.id, body); if (b.dataset.action === 'edit') openEdit(x.id); else if (b.dataset.action === 'delete') openInstanceDelete(x.id); else if (x.status === 'running') { x.status = 'stopping'; x.statusText = '停止中'; renderRows(); setTimeout(() => { x.status = 'exited'; x.statusText = '已退出'; x.pid = '—'; x.port = 0; renderRows(); showToast(`“${x.name}”已优雅退出。`); }, 700); } else openConfirm(x.id, 'start'); }));
body.appendChild(tr);
});
$('emptyState').style.display = rows.length ? 'none' : 'block'; $('resultCount').textContent = `显示 ${rows.length} 个实例`;
@@ -401,6 +404,14 @@
function showDrawer() { $('drawer').classList.add('open'); $('backdrop').classList.add('open'); $('drawer').setAttribute('aria-hidden', 'false'); setTimeout(() => $('drawer').querySelector('button, input, select')?.focus(), 60); }
function hideDrawer() { $('drawer').classList.remove('open'); $('backdrop').classList.remove('open'); $('drawer').setAttribute('aria-hidden', 'true'); }
function openConfirm(id, action) { const x = instances.find(i => i.id === id); modalAction = action; $('dialogTitle').textContent = action === 'restart' ? '重启浏览器实例?' : action === 'start' ? '启动浏览器实例?' : '关闭浏览器实例?'; $('dialogText').textContent = action === 'restart' ? `将先请求“${x.name}”正常退出,再使用相同配置重新启动。` : action === 'start' ? `将使用已保存配置启动“${x.name}”。` : `将请求“${x.name}”正常退出。未保存的网页内容由浏览器自行处理。`; $('dialogWarning').hidden = action !== false; $('dialogConfirm').textContent = action === false ? '优雅关闭' : action === 'restart' ? '确认重启' : '启动实例'; $('dialogConfirm').className = action === false ? 'danger-button' : 'primary'; $('confirmDialog').showModal(); }
function canDeleteInstanceData(x) { return ['exited', 'failed'].includes(x.status); }
function updateInstanceDeleteChoice() { const x = instances.find(i => i.id === pendingInstanceDeleteID), checked = $('deleteInstanceData').checked && !$('deleteInstanceData').disabled; $('instanceDeleteWarning').hidden = !checked; $('instanceDeleteWarning').textContent = checked && x ? `将永久删除:${x.dir}。此操作无法撤销;Chub 仍会在执行前重新检查浏览器占用。` : ''; $('instanceDeleteConfirm').textContent = checked ? '删除实例及数据' : '删除实例'; }
function openInstanceDelete(id) { const x = instances.find(i => i.id === id); if (!x) return; pendingInstanceDeleteID = id; instanceDeleteRunning = false; const allowed = canDeleteInstanceData(x); $('instanceDeleteText').textContent = `确定删除“${x.name}”的 Chub 实例配置吗?默认会保留 User Data Dir。`; $('deleteInstanceData').checked = false; $('deleteInstanceData').disabled = !allowed; $('instanceDeleteUnavailable').hidden = allowed; $('instanceDeleteUnavailable').textContent = allowed ? '' : '检测到浏览器正在运行、启动/停止或占用;请先停止并刷新状态后再删除数据。当前仍可只删除实例配置。'; $('instanceDeleteCancel').disabled = false; $('instanceDeleteConfirm').disabled = false; updateInstanceDeleteChoice(); $('instanceDeleteDialog').showModal(); setTimeout(() => $('instanceDeleteCancel').focus(), 0); }
function closeInstanceDelete() { if (instanceDeleteRunning) return; $('instanceDeleteDialog').close(); pendingInstanceDeleteID = ''; }
$('deleteInstanceData').onchange = updateInstanceDeleteChoice;
$('instanceDeleteCancel').onclick = closeInstanceDelete;
$('instanceDeleteDialog').addEventListener('cancel', e => { e.preventDefault(); closeInstanceDelete(); });
$('instanceDeleteConfirm').onclick = () => { const x = instances.find(i => i.id === pendingInstanceDeleteID), withData = $('deleteInstanceData').checked && !$('deleteInstanceData').disabled; if (!x || instanceDeleteRunning) return; if (!withData) { instances.splice(instances.indexOf(x), 1); $('instanceDeleteDialog').close(); pendingInstanceDeleteID = ''; renderRows(); showToast(`已删除“${x.name}”的实例配置;User Data Dir 未被删除。`); return; } instanceDeleteRunning = true; $('instanceDeleteCancel').disabled = true; $('instanceDeleteConfirm').disabled = true; $('instanceDeleteConfirm').textContent = '正在删除数据…'; setTimeout(() => { const current = instances.find(i => i.id === pendingInstanceDeleteID); if (current) instances.splice(instances.indexOf(current), 1); $('instanceDeleteDialog').close(); pendingInstanceDeleteID = ''; instanceDeleteRunning = false; renderRows(); showToast(`原型演示:已删除“${x.name}”的实例配置及 User Data Dir 数据。`); }, 700); };
function showToast(message) { const toast = $('toast'); toast.textContent = message; toast.classList.add('show'); clearTimeout(showToast.timer); showToast.timer = setTimeout(() => toast.classList.remove('show'), 3600); }
function hasOtherModal() { return [...document.querySelectorAll('dialog[open]')].some(dialog => dialog.id !== 'unexpectedExitDialog'); }
function queueUnexpectedExit(id) {