374 lines
49 KiB
HTML
374 lines
49 KiB
HTML
<!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; width: min(468px, 50vw); }
|
||
.instance-commands { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px; flex: 1; }
|
||
.instance-commands button { width: 100%; }
|
||
.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.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: 10px; opacity: .7; }
|
||
tr:hover .row-actions, tr.selected .row-actions { opacity: 1; }
|
||
.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; }
|
||
.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.edit-dialog { width: min(620px, calc(100vw - 32px)); }
|
||
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; }
|
||
.edit-form { display: grid; gap: 18px; }
|
||
.edit-form .path-field { grid-template-columns: minmax(0, 1fr) auto; }
|
||
.readonly-value { background: var(--surface-3) !important; color: var(--muted) !important; cursor: default; }
|
||
.form-note { padding: 10px 12px; border-radius: 9px; background: var(--warning-soft); color: var(--warning); font-size: 12px; line-height: 1.5; }
|
||
.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; width: 100%; }
|
||
.instance-commands { grid-template-columns: 1fr; }
|
||
.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><div class="instance-commands" id="instanceCommands"><button class="primary" id="newInstance">+ 新建实例</button><button class="secondary" id="refresh" title="刷新实例状态 (F5)">↻ 刷新状态</button></div></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="associated">外部已关联</option><option value="occupied">被占用</option><option value="exited">已退出</option></select></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><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="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>
|
||
</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>
|
||
<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>
|
||
const proxies = [
|
||
{ id: 'proxy-sg', name: '新加坡出口', server: 'http://127.0.0.1:8080' },
|
||
{ id: 'proxy-test', name: '测试 SOCKS5', server: 'socks5://127.0.0.1:1080' }
|
||
];
|
||
const instances = [
|
||
{ id: 'inst-chrome-shop', name: 'Shopee · SG 主店', kind: 'chrome', browser: 'Chrome', status: 'running', statusText: '运行中(托管)', pid: '18420', port: '9666', dir: 'D:\\Chub\\profiles\\shopee-sg', started: '今天 09:42', url: 'https://seller.shopee.sg/', proxyId: 'proxy-sg', executable: 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe' },
|
||
{ id: 'inst-edge-review', name: '素材审核 · Edge', kind: 'edge', browser: 'Edge', status: 'starting', statusText: '启动中', pid: '—', port: '检测中…', 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: 'associated', statusText: '外部已关联', pid: '16108', port: '9668', 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: '—', port: '—', 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';
|
||
let editingID = '';
|
||
let editSnapshot = null;
|
||
let selectedProxyID = '';
|
||
const $ = (id) => document.getElementById(id);
|
||
const statusClass = (s) => s;
|
||
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));
|
||
}
|
||
function selectRow(id, body) {
|
||
selectedId = id;
|
||
body.querySelectorAll('tr').forEach(row => row.classList.toggle('selected', row.dataset.id === id));
|
||
}
|
||
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.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="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); } });
|
||
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 = '—'; renderRows(); showToast(`“${x.name}”已优雅退出。`); }, 700); } 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), externallyAssociated = x.status === 'associated'; $('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 class="mono">${x.port || '—'}</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' || externallyAssociated ? 'disabled' : ''}>↻ 重启</button><button class="danger-button" id="stopBtn" ${x.status === 'exited' || externallyAssociated ? 'disabled' : ''}>关闭实例</button></div>${externallyAssociated ? '<small class="help">外部关联实例仅供观察,Chub 不会接管其生命周期。</small>' : ''}</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>${externallyAssociated ? '已验证本地 CDP 端点;未接管外部浏览器。' : 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 editIsLocked(x) { return ['running', 'starting', 'associated'].includes(x.status); }
|
||
function editIsDirty() {
|
||
if (!editingID || !editSnapshot) return false;
|
||
return $('editName').value !== editSnapshot.name || $('editDataDir').value !== editSnapshot.dir || $('editURL').value !== editSnapshot.url || $('editProxy').value !== editSnapshot.proxyId || document.querySelector('input[name="editKind"]:checked')?.value !== editSnapshot.kind;
|
||
}
|
||
function focusEditedRow(id) { requestAnimationFrame(() => document.querySelector(`tr[data-id="${id}"]`)?.focus()); }
|
||
function finishEdit() { const id = editingID; $('editDialog').close(); editingID = ''; editSnapshot = null; focusEditedRow(id); }
|
||
function openEdit(id) {
|
||
const x = instances.find(i => i.id === id); if (!x) return;
|
||
selectedId = id; editingID = id; editSnapshot = { name: x.name, dir: x.dir, url: x.url || '', kind: x.kind, proxyId: x.proxyId || '' };
|
||
const locked = editIsLocked(x); $('editName').value = x.name; $('editDataDir').value = x.dir; $('editURL').value = x.url || ''; renderProxyOptions($('editProxy'), x.proxyId || ''); $('editPort').value = x.port || '—'; $('editChrome').checked = x.kind === 'chrome'; $('editEdge').checked = x.kind === 'edge';
|
||
$('editSubtitle').textContent = locked ? '当前运行状态下可查看配置,但身份约束字段不可修改。' : '修改下次启动使用的保存配置。';
|
||
$('editChrome').disabled = locked; $('editEdge').disabled = locked; $('editDataDir').disabled = locked; $('chooseEditDir').disabled = locked; $('editReadOnlyNote').hidden = !locked;
|
||
$('editDialog').showModal(); setTimeout(() => $('editName').focus(), 0);
|
||
}
|
||
function saveEdit() {
|
||
const x = instances.find(i => i.id === editingID); if (!x) return finishEdit();
|
||
const name = $('editName').value.trim(), dir = $('editDataDir').value.trim(), url = $('editURL').value.trim();
|
||
if (!name || !dir) { showToast('请填写实例名称和 User Data Dir。'); (!name ? $('editName') : $('editDataDir')).focus(); return; }
|
||
const locked = editIsLocked(x); x.name = name; x.url = url; x.proxyId = $('editProxy').value;
|
||
if (!locked) { x.dir = dir; x.kind = document.querySelector('input[name="editKind"]:checked')?.value || x.kind; x.browser = x.kind === 'edge' ? 'Edge' : 'Chrome'; }
|
||
if ($('discardDialog').open) $('discardDialog').close(); renderRows(); const savedName = x.name; finishEdit(); showToast(`已保存“${savedName}”的实例配置。`);
|
||
}
|
||
function requestEditClose() { if (editIsDirty()) { $('editDialog').close(); $('discardDialog').showModal(); return; } finishEdit(); }
|
||
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="launchProxy">代理</label><select id="launchProxy"><option value="">无代理</option></select><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(); renderProxyOptions($('launchProxy')); $('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; } const proxy = proxyLabel($('launchProxy').value); hideDrawer(); setTimeout(() => showToast(`启动请求已提交(${proxy}),实例进入 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 refreshInstances() { const button = $('refresh'); if (button.disabled) return; button.disabled = true; button.textContent = '↻ 刷新中…'; setTimeout(() => { const starting = instances.find(x => x.status === 'starting'); if (starting) { starting.status = 'running'; starting.statusText = '运行中(托管)'; starting.pid = '18468'; starting.port = '9667'; } renderRows(); button.disabled = false; button.textContent = '↻ 刷新状态'; document.querySelector('.table-footer span:last-child').textContent = '最后检查:刚刚'; showToast('已检查保存的实例状态;未扫描其他 Chrome 或 Edge。'); }, 900); }
|
||
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 环境'; $('instanceCommands').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 = refreshInstances; $('themeToggle').onclick = () => { const dark = document.documentElement.dataset.theme === 'dark'; document.documentElement.dataset.theme = dark ? '' : 'dark'; $('themeToggle').textContent = dark ? '☾' : '☀'; $('themeToggle').setAttribute('aria-label', dark ? '切换深色主题' : '切换浅色主题'); };
|
||
$('editCancel').onclick = requestEditClose; $('chooseEditDir').onclick = () => showToast('原型演示:将打开目录选择器并回填 User Data Dir。'); $('editForm').addEventListener('submit', e => { e.preventDefault(); saveEdit(); }); $('editDialog').addEventListener('cancel', e => { e.preventDefault(); requestEditClose(); }); $('keepEditing').onclick = () => { $('discardDialog').close(); $('editDialog').showModal(); setTimeout(() => $('editName').focus(), 0); }; $('discardChanges').onclick = () => { $('discardDialog').close(); finishEdit(); }; $('saveChanges').onclick = saveEdit;
|
||
document.addEventListener('keydown', e => { if (e.key === 'F5' && !$('editDialog').open && !$('discardDialog').open) { e.preventDefault(); refreshInstances(); } });
|
||
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)); });
|
||
$('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();
|
||
</script>
|
||
</body>
|
||
</html>
|