2026-07-22 14:51:11 +08:00
<!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 18 px 48 px rgba ( 26 , 43 , 73 , .14 ), 0 2 px 7 px rgba ( 26 , 43 , 73 , .08 );
--radius-control : 8 px ;
--radius-card : 14 px ;
--radius-dialog : 16 px ;
--focus : 0 0 0 3 px 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 18 px 48 px 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 : 14 px ; }
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 : 5 px 14 px ; border-radius : 0 0 8 px 8 px ; font-size : 11 px ; letter-spacing : .04 em ; }
. app { display : grid ; grid-template-columns : 232 px minmax ( 0 , 1 fr ); min-height : 100 vh ; }
. sidebar { background : var ( -- surface ); border-right : 1 px solid var ( -- line ); padding : 34 px 14 px 18 px ; display : flex ; flex-direction : column ; gap : 24 px ; }
. brand { display : flex ; align-items : center ; gap : 10 px ; padding : 0 10 px ; }
. brand-mark { width : 32 px ; height : 32 px ; border-radius : 10 px ; background : linear-gradient ( 135 deg , #2563eb , #66a1ff ); color : #fff ; display : grid ; place-items : center ; font-weight : 800 ; letter-spacing : -.04 em ; }
. brand strong { display : block ; font-size : 17 px ; letter-spacing : -.02 em ; }
. brand span { color : var ( -- muted ); font-size : 11 px ; }
. nav { display : grid ; gap : 4 px ; }
. nav button { min-height : 40 px ; padding : 0 12 px ; text-align : left ; color : var ( -- muted ); background : transparent ; border-radius : var ( -- radius - control ); display : flex ; align-items : center ; gap : 11 px ; }
. 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 : 18 px ; text-align : center ; font-size : 16 px ; }
. sidebar-footer { margin-top : auto ; border-top : 1 px solid var ( -- line ); padding : 16 px 10 px 0 ; color : var ( -- muted ); font-size : 12 px ; line-height : 1.5 ; }
. main { min-width : 0 ; padding : 34 px clamp ( 18 px , 3 vw , 42 px ) 38 px ; }
. topbar { display : flex ; align-items : flex-start ; justify-content : space-between ; gap : 18 px ; max-width : 1420 px ; margin : 0 auto 24 px ; }
. eyebrow { color : var ( -- accent - strong ); font-size : 12 px ; font-weight : 700 ; letter-spacing : .08 em ; text-transform : uppercase ; }
h1 { margin : 5 px 0 6 px ; font-size : clamp ( 24 px , 3 vw , 32 px ); letter-spacing : -.035 em ; }
. subtitle { margin : 0 ; color : var ( -- muted ); }
. top-actions { display : flex ; gap : 8 px ; align-items : center ; }
. icon-button , . secondary , . primary , . danger-button { min-height : 36 px ; padding : 0 14 px ; border-radius : var ( -- radius - control ); font-weight : 650 ; white-space : nowrap ; }
. icon-button , . secondary { color : var ( -- text ); background : var ( -- surface ); border : 1 px solid var ( -- line - strong ); }
. icon-button { width : 38 px ; 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 : 1420 px ; margin : auto ; display : grid ; gap : 18 px ; }
. banner { display : flex ; gap : 12 px ; align-items : flex-start ; background : var ( -- warning - soft ); border : 1 px solid color-mix ( in srgb , var ( -- warning ) 30 % , transparent ); border-radius : var ( -- radius - card ); padding : 13 px 16 px ; color : var ( -- text ); }
. banner b { color : var ( -- warning ); }
. banner p { margin : 3 px 0 0 ; color : var ( -- muted ); font-size : 13 px ; }
. summary-grid { display : grid ; grid-template-columns : repeat ( 4 , minmax ( 0 , 1 fr )); gap : 12 px ; }
. summary { background : var ( -- surface ); border : 1 px solid var ( -- line ); border-radius : var ( -- radius - card ); padding : 16 px ; }
. summary-label { color : var ( -- muted ); font-size : 12 px ; }
. summary-value { margin-top : 8 px ; display : flex ; align-items : baseline ; gap : 8 px ; font-size : 25 px ; font-weight : 750 ; letter-spacing : -.03 em ; }
. summary-note { color : var ( -- muted ); font-size : 12 px ; }
. workspace { background : var ( -- surface ); border : 1 px solid var ( -- line ); border-radius : var ( -- radius - card ); overflow : hidden ; }
. toolbar { padding : 14 px 16 px ; border-bottom : 1 px solid var ( -- line ); display : flex ; flex-wrap : wrap ; align-items : center ; gap : 9 px ; }
. search { min-width : 220 px ; flex : 1 ; position : relative ; }
. search input { width : 100 % ; height : 36 px ; border : 1 px solid var ( -- line - strong ); border-radius : var ( -- radius - control ); background : var ( -- surface -2 ); padding : 0 12 px 0 35 px ; color : var ( -- text ); }
. search span { position : absolute ; left : 12 px ; top : 8 px ; color : var ( -- muted ); }
select { height : 36 px ; color : var ( -- text ); background : var ( -- surface ); border : 1 px solid var ( -- line - strong ); border-radius : var ( -- radius - control ); padding : 0 30 px 0 11 px ; }
. toolbar-spacer { flex : 1 ; }
. table-wrap { overflow-x : auto ; }
table { width : 100 % ; border-collapse : collapse ; min-width : 760 px ; }
th { height : 42 px ; padding : 0 16 px ; text-align : left ; color : var ( -- muted ); font-size : 12 px ; font-weight : 650 ; background : var ( -- surface -2 ); border-bottom : 1 px solid var ( -- line ); }
td { padding : 14 px 16 px ; border-bottom : 1 px 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 : 10 px ; min-width : 190 px ; }
. browser-logo { width : 28 px ; height : 28 px ; display : grid ; place-items : center ; border-radius : 8 px ; color : #fff ; font-size : 12 px ; font-weight : 800 ; }
. chrome { background : conic-gradient ( #e84c3d 0 33 % , #f5c746 0 66 % , #36a269 0 ); }
. edge { background : linear-gradient ( 140 deg , #0c9b9e , #1768d2 ); }
. instance-name strong { display : block ; font-size : 14 px ; }
. instance-name small , . mono { color : var ( -- muted ); font-size : 12 px ; font-family : "Cascadia Mono" , Consolas , monospace ; }
. status { display : inline-flex ; align-items : center ; gap : 7 px ; border-radius : 999 px ; padding : 5 px 9 px ; font-size : 12 px ; font-weight : 700 ; white-space : nowrap ; }
. status :: before { content : "" ; width : 7 px ; height : 7 px ; 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 : 6 px ; opacity : .7 ; }
tr : hover . row-actions , tr . selected . row-actions { opacity : 1 ; }
. row-actions button { background : transparent ; color : var ( -- muted ); padding : 6 px 8 px ; border-radius : 6 px ; }
. row-actions button : hover { color : var ( -- accent - strong ); background : var ( -- accent - soft ); }
. table-footer { display : flex ; align-items : center ; justify-content : space-between ; padding : 12 px 16 px ; color : var ( -- muted ); font-size : 12 px ; }
. drawer-backdrop { position : fixed ; inset : 0 ; background : rgba ( 16 , 24 , 40 , .28 ); z-index : 10 ; opacity : 0 ; pointer-events : none ; transition : opacity .16 s ease ; }
. drawer-backdrop . open { opacity : 1 ; pointer-events : auto ; }
. drawer { position : fixed ; z-index : 11 ; top : 0 ; right : 0 ; bottom : 0 ; width : min ( 480 px , 100 vw ); background : var ( -- surface ); box-shadow : var ( -- shadow ); transform : translateX ( 100 % ); transition : transform .2 s ease ; overflow : auto ; }
. drawer . open { transform : translateX ( 0 ); }
. drawer-head { display : flex ; align-items : flex-start ; justify-content : space-between ; gap : 12 px ; padding : 28 px 26 px 20 px ; border-bottom : 1 px solid var ( -- line ); }
. drawer-head h2 { margin : 0 0 5 px ; font-size : 21 px ; letter-spacing : -.02 em ; }
. drawer-head p { margin : 0 ; color : var ( -- muted ); font-size : 13 px ; }
. drawer-body { padding : 22 px 26 px ; display : grid ; gap : 22 px ; }
. form-section { display : grid ; gap : 13 px ; }
. form-section h3 { margin : 0 ; font-size : 13 px ; color : var ( -- muted ); text-transform : uppercase ; letter-spacing : .06 em ; }
. field { display : grid ; gap : 6 px ; }
. field label { font-weight : 650 ; }
. field label span { color : var ( -- danger ); }
. field input , . field select { width : 100 % ; height : 38 px ; border : 1 px solid var ( -- line - strong ); background : var ( -- surface -2 ); color : var ( -- text ); border-radius : var ( -- radius - control ); padding : 0 11 px ; }
. field small , . help { color : var ( -- muted ); font-size : 12 px ; line-height : 1.45 ; }
. field . error input { border-color : var ( -- danger ); box-shadow : 0 0 0 2 px color-mix ( in srgb , var ( -- danger ) 18 % , transparent ); }
. field-error { color : var ( -- danger ); font-size : 12 px ; }
. check-line { display : flex ; gap : 9 px ; align-items : flex-start ; color : var ( -- text ); }
. check-line input { width : 16 px ; height : 16 px ; accent-color : var ( -- accent ); margin-top : 2 px ; }
. drawer-actions { display : flex ; justify-content : flex-end ; gap : 8 px ; border-top : 1 px solid var ( -- line ); padding-top : 18 px ; }
. detail-card { padding : 16 px ; background : var ( -- surface -2 ); border : 1 px solid var ( -- line ); border-radius : 12 px ; display : grid ; gap : 12 px ; }
. detail-line { display : flex ; justify-content : space-between ; gap : 18 px ; 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 : 9 px ; }
. activity-item { display : grid ; grid-template-columns : 9 px 1 fr ; gap : 9 px ; font-size : 12 px ; color : var ( -- muted ); }
. activity-item i { width : 8 px ; height : 8 px ; background : var ( -- accent ); border-radius : 50 % ; margin-top : 4 px ; }
dialog { border : 0 ; border-radius : var ( -- radius - dialog ); padding : 0 ; width : min ( 430 px , calc ( 100 vw - 32 px )); color : var ( -- text ); background : var ( -- surface ); box-shadow : var ( -- shadow ); }
dialog :: backdrop { background : rgba ( 16 , 24 , 40 , .42 ); }
. dialog-inner { padding : 24 px ; display : grid ; gap : 17 px ; }
. dialog-inner h2 { margin : 0 ; font-size : 19 px ; }
. dialog-inner p { margin : 0 ; color : var ( -- muted ); line-height : 1.55 ; }
. dialog-warning { padding : 11 px 12 px ; border-radius : 9 px ; background : var ( -- danger - soft ); color : var ( -- danger ); font-size : 12 px ; line-height : 1.45 ; }
. dialog-actions { display : flex ; justify-content : flex-end ; gap : 8 px ; }
. empty-state { display : none ; text-align : center ; padding : 48 px 20 px ; color : var ( -- muted ); }
. empty-state strong { display : block ; color : var ( -- text ); font-size : 16 px ; margin-bottom : 7 px ; }
. sr-only { position : absolute ; width : 1 px ; height : 1 px ; padding : 0 ; margin : -1 px ; overflow : hidden ; clip : rect ( 0 , 0 , 0 , 0 ); white-space : nowrap ; border : 0 ; }
. toast { position : fixed ; right : 22 px ; bottom : 22 px ; z-index : 30 ; max-width : min ( 420 px , calc ( 100 vw - 44 px )); padding : 12 px 15 px ; border : 1 px solid var ( -- line ); border-left : 4 px solid var ( -- accent ); border-radius : 10 px ; background : var ( -- surface ); box-shadow : var ( -- shadow ); color : var ( -- text ); opacity : 0 ; transform : translateY ( 8 px ); pointer-events : none ; transition : opacity .16 s ease , transform .16 s ease ; }
. toast . show { opacity : 1 ; transform : translateY ( 0 ); }
. page-hidden { display : none !important ; }
. settings-grid { display : grid ; grid-template-columns : minmax ( 0 , 1.2 fr ) minmax ( 260 px , .8 fr ); gap : 18 px ; }
. settings-card { background : var ( -- surface ); border : 1 px solid var ( -- line ); border-radius : var ( -- radius - card ); padding : 22 px ; display : grid ; gap : 20 px ; }
. settings-card h2 { margin : 0 ; font-size : 18 px ; letter-spacing : -.02 em ; }
. settings-card > p { margin : -12 px 0 0 ; color : var ( -- muted ); line-height : 1.5 ; }
2026-07-22 15:57:02 +08:00
. path-field { display : grid ; grid-template-columns : minmax ( 0 , 1 fr ) auto auto ; gap : 8 px ; }
. radio-row { display : flex ; flex-wrap : wrap ; gap : 16 px ; align-items : center ; min-height : 38 px ; }
. radio-row label { font-weight : 500 ; display : inline-flex ; gap : 7 px ; align-items : center ; }
2026-07-22 14:51:11 +08:00
. settings-status { min-height : 42 px ; padding : 11 px 12 px ; border-radius : 9 px ; background : var ( -- success - soft ); color : var ( -- success ); font-size : 12 px ; line-height : 1.45 ; }
. settings-status . warning { background : var ( -- warning - soft ); color : var ( -- warning ); }
. settings-actions { display : flex ; justify-content : flex-end ; gap : 8 px ; border-top : 1 px solid var ( -- line ); padding-top : 18 px ; }
. setting-list { display : grid ; gap : 12 px ; }
. setting-list . detail-line { padding-bottom : 12 px ; border-bottom : 1 px solid var ( -- line ); }
. setting-list . detail-line : last-child { border-bottom : 0 ; padding-bottom : 0 ; }
@ media ( max-width : 800px ) { . settings-grid { grid-template-columns : 1 fr ; } }
@ media ( max-width : 1007px ) {
. app { grid-template-columns : 70 px minmax ( 0 , 1 fr ); }
. sidebar { padding-inline : 10 px ; }
. 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 , 1 fr )); }
}
@ media ( max-width : 640px ) {
. app { display : block ; }
. sidebar { height : 58 px ; padding : 10 px 14 px ; border-right : 0 ; border-bottom : 1 px 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 : 38 px ; }
. nav button span : not ( . nav-icon ) { display : none ; }
. main { padding : 28 px 12 px ; }
. topbar { display : block ; }
. top-actions { margin-top : 16 px ; }
. top-actions . primary { flex : 1 ; }
. summary-grid { gap : 8 px ; }
. summary { padding : 12 px ; }
. summary-value { font-size : 21 px ; }
. 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 : .01 ms !important ; animation-duration : .01 ms !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 >
2026-07-22 16:22:59 +08:00
< 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 >
2026-07-22 14:51:11 +08:00
< 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 >
2026-07-22 15:57:02 +08:00
< 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 >
2026-07-22 14:51:11 +08:00
</ div >
< div class = "form-section" >< h3 > 默认目录</ h3 >
2026-07-22 15:57:02 +08:00
< 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 >
2026-07-22 14:51:11 +08:00
</ 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 = `
2026-07-22 16:22:59 +08:00
<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>` ;
2026-07-22 14:51:11 +08:00
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' ); }
2026-07-22 15:57:02 +08:00
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 ); }; }
2026-07-22 14:51:11 +08:00
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' ) ? '文件' : '目录' } 选择器。` ));
2026-07-22 15:57:02 +08:00
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 )); });
2026-07-22 14:51:11 +08:00
$ ( '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 >