Files
cdp_hub/docs/ui/browser-manager.html
T

316 lines
36 KiB
HTML
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.
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Chub · 浏览器实例管理原型</title>
<style>
:root {
color-scheme: light;
--bg: #f5f7fb;
--surface: #ffffff;
--surface-2: #f8f9fc;
--surface-3: #eef2f8;
--text: #18202b;
--muted: #617083;
--line: #dce2eb;
--line-strong: #c7d0dd;
--accent: #2563eb;
--accent-strong: #1d4ed8;
--accent-soft: #e7efff;
--success: #087443;
--success-soft: #e4f6ed;
--warning: #9a5b00;
--warning-soft: #fff3d8;
--danger: #b42318;
--danger-soft: #ffebe8;
--shadow: 0 18px 48px rgba(26, 43, 73, .14), 0 2px 7px rgba(26, 43, 73, .08);
--radius-control: 8px;
--radius-card: 14px;
--radius-dialog: 16px;
--focus: 0 0 0 3px rgba(37, 99, 235, .25);
}
[data-theme="dark"] {
color-scheme: dark;
--bg: #11151c;
--surface: #1b222d;
--surface-2: #202936;
--surface-3: #293544;
--text: #f1f5fb;
--muted: #aab7c8;
--line: #364354;
--line-strong: #4a5a6c;
--accent: #83aaff;
--accent-strong: #a9c2ff;
--accent-soft: #263b66;
--success: #65d49a;
--success-soft: #173c2b;
--warning: #f6c15c;
--warning-soft: #4a3612;
--danger: #ff9c91;
--danger-soft: #4b2421;
--shadow: 0 18px 48px rgba(0, 0, 0, .35);
}
* { box-sizing: border-box; }
html, body { margin: 0; min-height: 100%; font-family: "Segoe UI Variable", "Segoe UI", system-ui, sans-serif; color: var(--text); background: var(--bg); }
body { font-size: 14px; }
button, input, select { font: inherit; }
button { border: 0; cursor: pointer; }
button:focus-visible, input:focus-visible, select:focus-visible { outline: none; box-shadow: var(--focus); }
.prototype-ribbon { position: fixed; z-index: 20; top: 0; left: 50%; transform: translateX(-50%); background: #173b72; color: #fff; padding: 5px 14px; border-radius: 0 0 8px 8px; font-size: 11px; letter-spacing: .04em; }
.app { display: grid; grid-template-columns: 232px minmax(0, 1fr); min-height: 100vh; }
.sidebar { background: var(--surface); border-right: 1px solid var(--line); padding: 34px 14px 18px; display: flex; flex-direction: column; gap: 24px; }
.brand { display: flex; align-items: center; gap: 10px; padding: 0 10px; }
.brand-mark { width: 32px; height: 32px; border-radius: 10px; background: linear-gradient(135deg, #2563eb, #66a1ff); color: #fff; display: grid; place-items: center; font-weight: 800; letter-spacing: -.04em; }
.brand strong { display: block; font-size: 17px; letter-spacing: -.02em; }
.brand span { color: var(--muted); font-size: 11px; }
.nav { display: grid; gap: 4px; }
.nav button { min-height: 40px; padding: 0 12px; text-align: left; color: var(--muted); background: transparent; border-radius: var(--radius-control); display: flex; align-items: center; gap: 11px; }
.nav button:hover { background: var(--surface-2); color: var(--text); }
.nav button[aria-current="page"] { background: var(--accent-soft); color: var(--accent-strong); font-weight: 650; }
.nav-icon { width: 18px; text-align: center; font-size: 16px; }
.sidebar-footer { margin-top: auto; border-top: 1px solid var(--line); padding: 16px 10px 0; color: var(--muted); font-size: 12px; line-height: 1.5; }
.main { min-width: 0; padding: 34px clamp(18px, 3vw, 42px) 38px; }
.topbar { display: flex; align-items: flex-start; justify-content: space-between; gap: 18px; max-width: 1420px; margin: 0 auto 24px; }
.eyebrow { color: var(--accent-strong); font-size: 12px; font-weight: 700; letter-spacing: .08em; text-transform: uppercase; }
h1 { margin: 5px 0 6px; font-size: clamp(24px, 3vw, 32px); letter-spacing: -.035em; }
.subtitle { margin: 0; color: var(--muted); }
.top-actions { display: flex; gap: 8px; align-items: center; }
.icon-button, .secondary, .primary, .danger-button { min-height: 36px; padding: 0 14px; border-radius: var(--radius-control); font-weight: 650; white-space: nowrap; }
.icon-button, .secondary { color: var(--text); background: var(--surface); border: 1px solid var(--line-strong); }
.icon-button { width: 38px; padding: 0; }
.secondary:hover, .icon-button:hover { background: var(--surface-2); }
.primary { color: #fff; background: var(--accent); }
.primary:hover { background: var(--accent-strong); }
.danger-button { color: #fff; background: var(--danger); }
.danger-button:hover { filter: brightness(.94); }
.content { max-width: 1420px; margin: auto; display: grid; gap: 18px; }
.banner { display: flex; gap: 12px; align-items: flex-start; background: var(--warning-soft); border: 1px solid color-mix(in srgb, var(--warning) 30%, transparent); border-radius: var(--radius-card); padding: 13px 16px; color: var(--text); }
.banner b { color: var(--warning); }
.banner p { margin: 3px 0 0; color: var(--muted); font-size: 13px; }
.summary-grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 12px; }
.summary { background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius-card); padding: 16px; }
.summary-label { color: var(--muted); font-size: 12px; }
.summary-value { margin-top: 8px; display: flex; align-items: baseline; gap: 8px; font-size: 25px; font-weight: 750; letter-spacing: -.03em; }
.summary-note { color: var(--muted); font-size: 12px; }
.workspace { background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius-card); overflow: hidden; }
.toolbar { padding: 14px 16px; border-bottom: 1px solid var(--line); display: flex; flex-wrap: wrap; align-items: center; gap: 9px; }
.search { min-width: 220px; flex: 1; position: relative; }
.search input { width: 100%; height: 36px; border: 1px solid var(--line-strong); border-radius: var(--radius-control); background: var(--surface-2); padding: 0 12px 0 35px; color: var(--text); }
.search span { position: absolute; left: 12px; top: 8px; color: var(--muted); }
select { height: 36px; color: var(--text); background: var(--surface); border: 1px solid var(--line-strong); border-radius: var(--radius-control); padding: 0 30px 0 11px; }
.toolbar-spacer { flex: 1; }
.table-wrap { overflow-x: auto; }
table { width: 100%; border-collapse: collapse; min-width: 760px; }
th { height: 42px; padding: 0 16px; text-align: left; color: var(--muted); font-size: 12px; font-weight: 650; background: var(--surface-2); border-bottom: 1px solid var(--line); }
td { padding: 14px 16px; border-bottom: 1px solid var(--line); vertical-align: middle; }
tbody tr { cursor: pointer; }
tbody tr:hover { background: var(--surface-2); }
tbody tr.selected { background: var(--accent-soft); }
.instance-name { display: flex; align-items: center; gap: 10px; min-width: 190px; }
.browser-logo { width: 28px; height: 28px; display: grid; place-items: center; border-radius: 8px; color: #fff; font-size: 12px; font-weight: 800; }
.chrome { background: conic-gradient(#e84c3d 0 33%, #f5c746 0 66%, #36a269 0); }
.edge { background: linear-gradient(140deg, #0c9b9e, #1768d2); }
.instance-name strong { display: block; font-size: 14px; }
.instance-name small, .mono { color: var(--muted); font-size: 12px; font-family: "Cascadia Mono", Consolas, monospace; }
.status { display: inline-flex; align-items: center; gap: 7px; border-radius: 999px; padding: 5px 9px; font-size: 12px; font-weight: 700; white-space: nowrap; }
.status::before { content: ""; width: 7px; height: 7px; border-radius: 50%; background: currentColor; }
.status.running { color: var(--success); background: var(--success-soft); }
.status.starting { color: var(--accent-strong); background: var(--accent-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; }
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: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; }
.drawer-backdrop.open { opacity: 1; pointer-events: auto; }
.drawer { position: fixed; z-index: 11; top: 0; right: 0; bottom: 0; width: min(480px, 100vw); background: var(--surface); box-shadow: var(--shadow); transform: translateX(100%); transition: transform .2s ease; overflow: auto; }
.drawer.open { transform: translateX(0); }
.drawer-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; padding: 28px 26px 20px; border-bottom: 1px solid var(--line); }
.drawer-head h2 { margin: 0 0 5px; font-size: 21px; letter-spacing: -.02em; }
.drawer-head p { margin: 0; color: var(--muted); font-size: 13px; }
.drawer-body { padding: 22px 26px; display: grid; gap: 22px; }
.form-section { display: grid; gap: 13px; }
.form-section h3 { margin: 0; font-size: 13px; color: var(--muted); text-transform: uppercase; letter-spacing: .06em; }
.field { display: grid; gap: 6px; }
.field label { font-weight: 650; }
.field label span { color: var(--danger); }
.field input, .field select { width: 100%; height: 38px; border: 1px solid var(--line-strong); background: var(--surface-2); color: var(--text); border-radius: var(--radius-control); padding: 0 11px; }
.field small, .help { color: var(--muted); font-size: 12px; line-height: 1.45; }
.field.error input { border-color: var(--danger); box-shadow: 0 0 0 2px color-mix(in srgb, var(--danger) 18%, transparent); }
.field-error { color: var(--danger); font-size: 12px; }
.check-line { display: flex; gap: 9px; align-items: flex-start; color: var(--text); }
.check-line input { width: 16px; height: 16px; accent-color: var(--accent); margin-top: 2px; }
.drawer-actions { display: flex; justify-content: flex-end; gap: 8px; border-top: 1px solid var(--line); padding-top: 18px; }
.detail-card { padding: 16px; background: var(--surface-2); border: 1px solid var(--line); border-radius: 12px; display: grid; gap: 12px; }
.detail-line { display: flex; justify-content: space-between; gap: 18px; align-items: baseline; }
.detail-line span:first-child { color: var(--muted); }
.detail-line span:last-child { text-align: right; max-width: 68%; overflow-wrap: anywhere; }
.activity { display: grid; gap: 9px; }
.activity-item { display: grid; grid-template-columns: 9px 1fr; gap: 9px; font-size: 12px; color: var(--muted); }
.activity-item i { width: 8px; height: 8px; background: var(--accent); border-radius: 50%; margin-top: 4px; }
dialog { border: 0; border-radius: var(--radius-dialog); padding: 0; width: min(430px, calc(100vw - 32px)); color: var(--text); background: var(--surface); box-shadow: var(--shadow); }
dialog::backdrop { background: rgba(16, 24, 40, .42); }
.dialog-inner { padding: 24px; display: grid; gap: 17px; }
.dialog-inner h2 { margin: 0; font-size: 19px; }
.dialog-inner p { margin: 0; color: var(--muted); line-height: 1.55; }
.dialog-warning { padding: 11px 12px; border-radius: 9px; background: var(--danger-soft); color: var(--danger); font-size: 12px; line-height: 1.45; }
.dialog-actions { display: flex; justify-content: flex-end; gap: 8px; }
.empty-state { display: none; text-align: center; padding: 48px 20px; color: var(--muted); }
.empty-state strong { display: block; color: var(--text); font-size: 16px; margin-bottom: 7px; }
.sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0; }
.toast { position: fixed; right: 22px; bottom: 22px; z-index: 30; max-width: min(420px, calc(100vw - 44px)); padding: 12px 15px; border: 1px solid var(--line); border-left: 4px solid var(--accent); border-radius: 10px; background: var(--surface); box-shadow: var(--shadow); color: var(--text); opacity: 0; transform: translateY(8px); pointer-events: none; transition: opacity .16s ease, transform .16s ease; }
.toast.show { opacity: 1; transform: translateY(0); }
.page-hidden { display: none !important; }
.settings-grid { display: grid; grid-template-columns: minmax(0, 1.2fr) minmax(260px, .8fr); gap: 18px; }
.settings-card { background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius-card); padding: 22px; display: grid; gap: 20px; }
.settings-card h2 { margin: 0; font-size: 18px; letter-spacing: -.02em; }
.settings-card > p { margin: -12px 0 0; color: var(--muted); line-height: 1.5; }
.path-field { display: grid; grid-template-columns: minmax(0, 1fr) auto auto; gap: 8px; }
.radio-row { display: flex; flex-wrap: wrap; gap: 16px; align-items: center; min-height: 38px; }
.radio-row label { font-weight: 500; display: inline-flex; gap: 7px; align-items: center; }
.settings-status { min-height: 42px; padding: 11px 12px; border-radius: 9px; background: var(--success-soft); color: var(--success); font-size: 12px; line-height: 1.45; }
.settings-status.warning { background: var(--warning-soft); color: var(--warning); }
.settings-actions { display: flex; justify-content: flex-end; gap: 8px; border-top: 1px solid var(--line); padding-top: 18px; }
.setting-list { display: grid; gap: 12px; }
.setting-list .detail-line { padding-bottom: 12px; border-bottom: 1px solid var(--line); }
.setting-list .detail-line:last-child { border-bottom: 0; padding-bottom: 0; }
@media (max-width: 800px) { .settings-grid { grid-template-columns: 1fr; } }
@media (max-width: 1007px) {
.app { grid-template-columns: 70px minmax(0, 1fr); }
.sidebar { padding-inline: 10px; }
.brand { justify-content: center; padding: 0; }
.brand > div:last-child, .nav button span:not(.nav-icon), .sidebar-footer { display: none; }
.nav button { justify-content: center; padding: 0; }
.summary-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (max-width: 640px) {
.app { display: block; }
.sidebar { height: 58px; padding: 10px 14px; border-right: 0; border-bottom: 1px solid var(--line); flex-direction: row; align-items: center; }
.brand { margin-right: auto; }
.brand > div:last-child { display: block; }
.nav { display: flex; }
.nav button { width: 38px; }
.nav button span:not(.nav-icon) { display: none; }
.main { padding: 28px 12px; }
.topbar { display: block; }
.top-actions { margin-top: 16px; }
.top-actions .primary { flex: 1; }
.summary-grid { gap: 8px; }
.summary { padding: 12px; }
.summary-value { font-size: 21px; }
.toolbar { align-items: stretch; }
.search { min-width: 100%; }
.toolbar-spacer { display: none; }
.toolbar select { flex: 1; }
}
@media (prefers-reduced-motion: reduce) { *, *::before, *::after { transition-duration: .01ms !important; animation-duration: .01ms !important; } }
</style>
</head>
<body>
<div class="prototype-ribbon">PROTOTYPE · 仅供交互确认,非生产实现</div>
<div class="app" id="app">
<aside class="sidebar" aria-label="主导航">
<div class="brand"><div class="brand-mark" aria-hidden="true">C</div><div><strong>Chub</strong><span>Browser Hub</span></div></div>
<nav class="nav">
<button aria-current="page" data-nav="instances"><span class="nav-icon" aria-hidden="true">▦</span><span>实例</span></button>
<button data-nav="settings"><span class="nav-icon" aria-hidden="true">⚙</span><span>设置</span></button>
</nav>
<div class="sidebar-footer">本地运行<br>不保存浏览器凭证</div>
</aside>
<main class="main">
<header class="topbar">
<div><div class="eyebrow" id="pageEyebrow">Browser instances</div><h1 id="pageTitle">浏览器实例</h1><p class="subtitle" id="pageSubtitle">启动、观察并安全管理 Chrome 与 Edge 环境</p></div>
<div class="top-actions"><button class="icon-button" id="themeToggle" aria-label="切换深色主题" title="切换深色主题">☾</button><button class="primary" id="newInstance">+ 新建实例</button></div>
</header>
<section class="content" id="instancesPage" aria-label="实例工作区">
<div class="banner" role="status"><div aria-hidden="true">●</div><div><b>本地安全模式</b><p>Chub 只管理已注册且身份校验通过的实例。外部浏览器不会被自动关闭。</p></div></div>
<section class="summary-grid" aria-label="实例概览">
<article class="summary"><div class="summary-label">全部实例</div><div class="summary-value">4 <span class="summary-note">已配置</span></div></article>
<article class="summary"><div class="summary-label">运行中</div><div class="summary-value" style="color:var(--success)">1 <span class="summary-note">稳定</span></div></article>
<article class="summary"><div class="summary-label">启动中</div><div class="summary-value" style="color:var(--accent-strong)">1 <span class="summary-note">请稍候</span></div></article>
<article class="summary"><div class="summary-label">需处理</div><div class="summary-value" style="color:var(--warning)">1 <span class="summary-note">profile 占用</span></div></article>
</section>
<section class="workspace" aria-labelledby="listTitle">
<div class="toolbar"><div class="search"><span aria-hidden="true">⌕</span><input id="search" type="search" placeholder="搜索实例、路径或 PID" aria-label="搜索实例、路径或 PID"></div><select id="browserFilter" aria-label="筛选浏览器"><option value="all">全部浏览器</option><option value="chrome">Chrome</option><option value="edge">Edge</option></select><select id="statusFilter" aria-label="筛选状态"><option value="all">全部状态</option><option value="running">运行中</option><option value="starting">启动中</option><option value="occupied">被占用</option><option value="exited">已退出</option></select><div class="toolbar-spacer"></div><button class="secondary" id="refresh">↻ 刷新状态</button></div>
<div class="table-wrap"><table><caption id="listTitle" class="sr-only">浏览器实例列表</caption><thead><tr><th scope="col">实例名称</th><th scope="col">浏览器类型</th><th scope="col">用户数据目录</th><th scope="col">状态</th><th scope="col">启动</th></tr></thead><tbody id="instanceRows"></tbody></table><div class="empty-state" id="emptyState"><strong>没有匹配的实例</strong><span>尝试清除筛选,或新建一个浏览器实例。</span></div></div>
<div class="table-footer"><span id="resultCount">显示 4 个实例</span><span>最后检查:刚刚</span></div>
</section>
</section>
<section class="content page-hidden" id="settingsPage" aria-label="应用设置">
<div class="settings-grid">
<form class="settings-card" id="settingsForm">
<div><h2>浏览器与目录</h2><p>这些设置用于新建实例的默认值;已有实例的配置不会被静默修改。</p></div>
<div class="form-section"><h3>浏览器路径</h3>
<div class="field"><label for="chromePath">Chrome 可执行文件</label><div class="path-field"><input id="chromePath" value="自动发现:chrome.exe"><button type="button" class="secondary browse-setting" data-target="chromePath">选择文件</button><button type="button" class="secondary search-setting" data-target="chromePath">搜索</button></div><small>留空或选择文件后,Chub 启动时会校验路径可访问。</small></div>
<div class="field"><label for="edgePath">Edge 可执行文件</label><div class="path-field"><input id="edgePath" value="自动发现:msedge.exe"><button type="button" class="secondary browse-setting" data-target="edgePath">选择文件</button><button type="button" class="secondary search-setting" data-target="edgePath">搜索</button></div><small>Chrome 与 Edge 可分别设置,也可以继续使用自动发现。</small></div>
</div>
<div class="form-section"><h3>默认目录</h3>
<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="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><label class="check-line"><input type="checkbox" id="enableCdp"><span><b>允许本地 CDP</b><br><small class="help">仅绑定 loopback;MVP 默认关闭。</small></span></label><div class="settings-status" id="settingsStatus">设置已保存 · 最后更新:刚刚</div></div></aside>
</div>
</section>
</main>
</div>
<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>
<div class="toast" id="toast" role="status" aria-live="polite"></div>
<script>
const instances = [
{ id: 'inst-chrome-shop', name: 'Shopee · SG 主店', kind: 'chrome', browser: 'Chrome', status: 'running', statusText: '运行中', pid: '18420', dir: 'D:\\Chub\\profiles\\shopee-sg', started: '今天 09:42', url: 'https://seller.shopee.sg/', executable: 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe' },
{ id: 'inst-edge-review', name: '素材审核 · Edge', kind: 'edge', browser: 'Edge', status: 'starting', statusText: '启动中', pid: '—', dir: 'D:\\Chub\\profiles\\asset-review', started: '正在启动', url: 'https://example.com/review', executable: 'C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe' },
{ id: 'inst-chrome-ph', name: 'Shopee · PH 备用', kind: 'chrome', browser: 'Chrome', status: 'occupied', statusText: '被占用', pid: '16108', dir: 'D:\\Chub\\profiles\\shopee-ph', started: '昨天 18:16', url: 'https://seller.shopee.ph/', executable: 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe' },
{ id: 'inst-edge-test', name: '测试环境 · Edge', kind: 'edge', browser: 'Edge', status: 'exited', statusText: '已退出', pid: '—', dir: 'D:\\Chub\\profiles\\edge-test', started: '昨天 16:04', url: 'https://localhost:3000', executable: 'C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe', exit: '退出码 0' }
];
let selectedId = 'inst-chrome-shop';
let modalAction = 'stop';
const $ = (id) => document.getElementById(id);
const statusClass = (s) => s;
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.dir].join(' ').toLowerCase().includes(q)) && (browser === 'all' || x.kind === browser) && (status === 'all' || x.status === status));
}
function renderRows() {
const rows = filtered(), body = $('instanceRows'); body.innerHTML = '';
rows.forEach(x => { const tr = document.createElement('tr'); tr.className = x.id === selectedId ? 'selected' : ''; tr.tabIndex = 0; tr.dataset.id = x.id; 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><span class="status ${statusClass(x.status)}">${x.statusText}</span></td>
<td><div class="row-actions"><button data-action="start" aria-label="启动 ${x.name}">启动</button></div></td>`;
tr.addEventListener('click', e => { if (!e.target.closest('button')) openDetails(x.id); });
tr.addEventListener('keydown', e => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); openDetails(x.id); } });
tr.querySelectorAll('button').forEach(b => b.addEventListener('click', e => { e.stopPropagation(); selectedId = x.id; if (b.dataset.action === 'details') openDetails(x.id); else if (b.dataset.action === 'stop') openConfirm(x.id, false); else openConfirm(x.id, 'start'); }));
body.appendChild(tr);
});
$('emptyState').style.display = rows.length ? 'none' : 'block'; $('resultCount').textContent = `显示 ${rows.length} 个实例`;
}
function openDetails(id) { selectedId = id; const x = instances.find(i => i.id === id); $('drawer').innerHTML = `<div class="drawer-head"><div><h2>${x.name}</h2><p>${x.browser} · ${x.id}</p></div><button class="icon-button" id="closeDrawer" aria-label="关闭详情">×</button></div><div class="drawer-body"><div><span class="status ${x.status}">${x.statusText}</span></div><div class="detail-card"><div class="detail-line"><span>进程 PID</span><span class="mono">${x.pid}</span></div><div class="detail-line"><span>浏览器</span><span>${x.browser}</span></div><div class="detail-line"><span>user data dir</span><span class="mono">${x.dir}</span></div><div class="detail-line"><span>启动地址</span><span class="mono">${x.url}</span></div><div class="detail-line"><span>可执行文件</span><span class="mono">${x.executable}</span></div>${x.exit ? `<div class="detail-line"><span>退出结果</span><span>${x.exit}</span></div>` : ''}</div><div class="form-section"><h3>实例操作</h3><div class="drawer-actions" style="border-top:0;padding-top:0;justify-content:flex-start"><button class="secondary" id="restartBtn" ${x.status === 'starting' ? 'disabled' : ''}>↻ 重启</button><button class="danger-button" id="stopBtn" ${x.status === 'exited' ? 'disabled' : ''}>关闭实例</button></div></div><div class="form-section"><h3>最近活动</h3><div class="activity"><div class="activity-item"><i></i><span>状态已同步 · ${x.started}</span></div><div class="activity-item"><i></i><span>${x.status === 'occupied' ? '检测到外部进程占用该 user data dir' : '实例配置已通过身份校验'}</span></div></div></div></div>`; showDrawer(); $('closeDrawer').onclick = hideDrawer; $('stopBtn').onclick = () => openConfirm(x.id, false); $('restartBtn').onclick = () => openConfirm(x.id, 'restart'); }
function openLaunch() { $('drawer').innerHTML = `<div class="drawer-head"><div><h2>新建浏览器实例</h2><p>配置完成后,Chub 会在本机启动一个隔离环境。</p></div><button class="icon-button" id="closeDrawer" aria-label="关闭新建实例">×</button></div><form class="drawer-body" id="launchForm"><div class="form-section"><h3>浏览器配置</h3><div class="field"><span>浏览器类型 <span>*</span></span><div class="radio-row" role="radiogroup" aria-label="浏览器类型"><label><input type="radio" name="kind" value="chrome" checked> Chrome</label><label><input type="radio" name="kind" value="edge"> Edge</label></div></div><div class="field"><label for="exe">可执行文件</label><input id="exe" value="自动发现" aria-describedby="exeHelp"><small id="exeHelp">留空时按浏览器类型搜索标准安装路径。</small></div></div><div class="form-section"><h3>运行环境</h3><div class="field"><label for="dataDir">User Data Dir <span>*</span></label><div class="path-field"><input id="dataDir" placeholder="例如:D:\\Chub\\profiles\\new-instance" required><button type="button" class="secondary" id="chooseInstanceDir">选择目录</button></div><small>必须是绝对路径;同一目录不能被多个实例同时使用。</small></div><div class="field"><label for="profile">profile-directory</label><input id="profile" value="Default"></div><div class="field"><label for="url">启动 URL(可选)</label><input id="url" type="url" placeholder="https://example.com"><small>仅支持 http/https;留空时启动浏览器默认页。</small></div><div class="field"><label for="proxy">代理服务器</label><input id="proxy" placeholder="可选,例如 http://127.0.0.1:8080"><small>仅支持无认证代理,认证信息不会被保存。</small></div></div><div class="form-section"><h3>启动选项</h3><label class="check-line"><input type="checkbox" id="headless"> <span><b>无头模式</b><br><small class="help">启动后台浏览器,不显示窗口。</small></span></label><label class="check-line"><input type="checkbox" id="remember" checked> <span><b>保存此配置</b><br><small class="help">只保存路径和非敏感参数。</small></span></label></div><div class="drawer-actions"><button type="button" class="secondary" id="cancelLaunch">取消</button><button type="submit" class="primary">启动实例</button></div></form>`; showDrawer(); $('closeDrawer').onclick = hideDrawer; $('cancelLaunch').onclick = hideDrawer; $('chooseInstanceDir').onclick = () => showToast('原型演示:将打开目录选择器。'); $('launchForm').onsubmit = e => { e.preventDefault(); const data = $('dataDir'); if (!data.value.trim()) { data.focus(); data.parentElement.classList.add('error'); const msg = document.createElement('div'); msg.className = 'field-error'; msg.textContent = '请填写 User Data Dir。'; data.parentElement.appendChild(msg); return; } hideDrawer(); setTimeout(() => showToast('启动请求已提交,实例进入 STARTING 状态。'), 80); }; }
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 showToast(message) { const toast = $('toast'); toast.textContent = message; toast.classList.add('show'); clearTimeout(showToast.timer); showToast.timer = setTimeout(() => toast.classList.remove('show'), 3600); }
$('dialogCancel').onclick = () => $('confirmDialog').close(); $('confirmDialog').addEventListener('cancel', e => { e.preventDefault(); $('confirmDialog').close(); }); $('dialogConfirm').onclick = () => { $('confirmDialog').close(); const message = modalAction === false ? '关闭请求已提交,实例将进入 STOPPING。' : modalAction === 'restart' ? '重启请求已提交,正在等待旧实例退出。' : '启动请求已提交,实例将进入 STARTING。'; setTimeout(() => showToast(message), 80); };
function setPage(page) { const settings = page === 'settings'; $('instancesPage').classList.toggle('page-hidden', settings); $('settingsPage').classList.toggle('page-hidden', !settings); $('pageEyebrow').textContent = settings ? 'Application settings' : 'Browser instances'; $('pageTitle').textContent = settings ? '设置' : '浏览器实例'; $('pageSubtitle').textContent = settings ? '管理浏览器路径、默认目录和本地运行行为' : '启动、观察并安全管理 Chrome 与 Edge 环境'; $('newInstance').classList.toggle('page-hidden', settings); if (settings) $('chromePath').focus(); }
$('newInstance').onclick = openLaunch; $('backdrop').onclick = hideDrawer; $('search').oninput = renderRows; $('browserFilter').onchange = renderRows; $('statusFilter').onchange = renderRows; $('refresh').onclick = () => { $('refresh').textContent = '✓ 已刷新'; setTimeout(() => $('refresh').textContent = '↻ 刷新状态', 1200); }; $('themeToggle').onclick = () => { const dark = document.documentElement.dataset.theme === 'dark'; document.documentElement.dataset.theme = dark ? '' : 'dark'; $('themeToggle').textContent = dark ? '☾' : '☀'; $('themeToggle').setAttribute('aria-label', dark ? '切换深色主题' : '切换浅色主题'); };
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)); });
$('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();
</script>
</body>
</html>