feat(proxy): 分流规则页交互 — 代理模式切换 + 添加/导入/重置(两端)
原型路由页此前段选与三按钮无事件、不重画。补齐(desktop + mobile 同步): - 代理模式段选(全局代理/智能分流/全部直连)点击切换 + renderRouting 重画: 非智能分流时「国内直连 + 我的规则」灰掉禁点、FINAL 兜底按模式变(全局/智能→走隧道、 全部直连→直连)、模式说明随之更新。 - 添加规则→弹层表单(规则类型/目标/动作段选),提交插入规则行(首命中置顶)。 - 从文本导入→textarea,按「目标,动作」逐行解析插入。 - 重置默认→确认弹层,还原出厂规则表。 - 弹层内容 JS 双语渲染(state/S.lang),避免再造「EN 显中文」;复用现有 .dl-overlay/ .rt-sheet + segswitch 范式;图标全取 icons.js 单源(refresh-cw/x/plus/download)。 注:路由页静态标签(段选/内置/FINAL 等)的整体 i18n 仍是独立后续(subagent 已标记的 19 个未接 rr.* 词汇 key)。本次聚焦交互,未改 I18N 生成块(--check 绿)。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A79VtQA1BwTuQN1ThpvYpo
This commit is contained in:
@@ -740,19 +740,20 @@
|
||||
<div class="view-body settings-view">
|
||||
<button class="pv-back" id="rtBack"><svg><use href="#i-arrow-left"/></svg><span>返回设置</span></button>
|
||||
<div class="sec-label">代理模式</div>
|
||||
<div class="segswitch">
|
||||
<button class="segswitch-opt"><span>全局代理</span></button>
|
||||
<button class="segswitch-opt is-active"><span>智能分流</span></button>
|
||||
<button class="segswitch-opt"><span>全部直连</span></button>
|
||||
<div class="segswitch" id="rtModeSeg">
|
||||
<button class="segswitch-opt" data-mode="global"><span>全局代理</span></button>
|
||||
<button class="segswitch-opt is-active" data-mode="smart"><span>智能分流</span></button>
|
||||
<button class="segswitch-opt" data-mode="direct"><span>全部直连</span></button>
|
||||
</div>
|
||||
<p style="font-size:var(--text-caption);color:var(--fg3);margin:var(--space-2) 0 var(--space-5)">下方「我的规则」仅在<b>智能分流</b>模式生效。全局代理 / 全部直连为一刀切模式,忽略自定义规则(系统强制层仍保留)。</p>
|
||||
<p id="rtModeNote" style="font-size:var(--text-caption);color:var(--fg3);margin:var(--space-2) 0 var(--space-5)">下方「我的规则」仅在<b>智能分流</b>模式生效。全局代理 / 全部直连为一刀切模式,忽略自定义规则(系统强制层仍保留)。</p>
|
||||
<div class="sec-label">内置规则</div>
|
||||
<div class="card card-flush" style="margin-bottom:var(--space-5)">
|
||||
<div class="setting-row"><span class="icon-box"><svg><use href="#i-globe"/></svg></span><div class="st-body"><div class="st-title">国内直连</div><div class="st-sub">GeoIP / GeoSite CN</div></div><span class="st-trail"><button class="toggle is-on"><i></i></button></span></div>
|
||||
<div class="setting-row" id="rtCnRow"><span class="icon-box"><svg><use href="#i-globe"/></svg></span><div class="st-body"><div class="st-title">国内直连</div><div class="st-sub">GeoIP / GeoSite CN</div></div><span class="st-trail"><button class="toggle is-on"><i></i></button></span></div>
|
||||
<div class="setting-row"><span class="icon-box"><svg><use href="#i-home"/></svg></span><div class="st-body"><div class="st-title">局域网 / 私网直连</div><div class="st-sub">强制,不可关</div></div><span class="st-trail"><span class="pill is-neutral">🔒 强制</span></span></div>
|
||||
</div>
|
||||
<div class="sec-label">我的规则 · 有序,首命中生效</div>
|
||||
<div class="card card-flush" style="margin-bottom:var(--space-4)">
|
||||
<div id="rtMyRules">
|
||||
<div class="sec-label" id="rtMyRulesLabel">我的规则 · 有序,首命中生效</div>
|
||||
<div class="card card-flush" id="rtRulesList" style="margin-bottom:var(--space-4)">
|
||||
<div class="setting-row"><span style="color:var(--fg3);opacity:.55;margin-right:var(--space-2)">≡</span><div class="st-body"><div class="st-title" style="font-family:var(--font-mono);font-size:var(--text-sm)">git.51yanmei.com</div><div class="st-sub">域名后缀 · CI 源</div></div><span class="st-trail"><span class="pill is-success">直连</span></span></div>
|
||||
<div class="setting-row"><span style="color:var(--fg3);opacity:.55;margin-right:var(--space-2)">≡</span><div class="st-body"><div class="st-title" style="font-family:var(--font-mono);font-size:var(--text-sm)">aliyun.com</div><div class="st-sub">域名后缀 · 镜像</div></div><span class="st-trail"><span class="pill is-success">直连</span></span></div>
|
||||
<div class="setting-row"><span style="color:var(--fg3);opacity:.55;margin-right:var(--space-2)">≡</span><div class="st-body"><div class="st-title" style="font-family:var(--font-mono);font-size:var(--text-sm)">github.com</div><div class="st-sub">域名后缀</div></div><span class="st-trail"><span class="pill is-warning">走隧道</span></span></div>
|
||||
@@ -760,12 +761,13 @@
|
||||
<div class="setting-row" style="opacity:.5"><span style="color:var(--fg3);opacity:.55;margin-right:var(--space-2)">≡</span><div class="st-body"><div class="st-title" style="font-family:var(--font-mono);font-size:var(--text-sm)">git.yanmeiai.com</div><div class="st-sub" style="color:var(--warning)">⚠ 该域名由系统强制走隧道,此规则不生效</div></div><span class="st-trail"><span class="pill is-success">直连</span></span></div>
|
||||
</div>
|
||||
<div style="display:flex;gap:var(--space-3);align-items:center;margin-bottom:var(--space-5)">
|
||||
<button class="btn btn-subtle"><svg><use href="#i-plus"/></svg>添加规则</button>
|
||||
<button class="btn btn-ghost">从文本导入</button>
|
||||
<button class="btn btn-ghost">重置默认</button>
|
||||
<button class="btn btn-subtle" id="rtAddBtn"><svg><use href="#i-plus"/></svg>添加规则</button>
|
||||
<button class="btn btn-ghost" id="rtImportBtn">从文本导入</button>
|
||||
<button class="btn btn-ghost" id="rtResetBtn">重置默认</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="sec-label">最终(未命中任何规则)</div>
|
||||
<div class="card card-flush"><div class="setting-row"><div class="st-body"><div class="st-title">FINAL 兜底</div></div><span class="st-trail"><span class="pill is-warning">走隧道</span></span></div></div>
|
||||
<div class="card card-flush"><div class="setting-row"><div class="st-body"><div class="st-title">FINAL 兜底</div></div><span class="st-trail"><span class="pill is-warning" id="rtFinalPill">走隧道</span></span></div></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -1119,6 +1121,10 @@
|
||||
<button class="limit-logout js-dl-close" data-i18n="dl.logout">退出登录</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 分流规则:添加 / 导入 / 重置 弹层(内容由 JS 双语渲染) -->
|
||||
<div class="dl-overlay" id="rtAddModal"><div class="limit-win" id="rtAddWin"></div></div>
|
||||
<div class="dl-overlay" id="rtImportModal"><div class="limit-win" id="rtImportWin"></div></div>
|
||||
<div class="dl-overlay" id="rtResetModal"><div class="limit-win" id="rtResetWin"></div></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2006,8 +2012,108 @@ var _accRedeem=document.getElementById('accRedeemRow'); if(_accRedeem) _accRedee
|
||||
var _accInvite=document.getElementById('accInviteRow'); if(_accInvite) _accInvite.addEventListener('click', function(){ setView('invite'); });
|
||||
var _accDevices=document.getElementById('accDevicesRow'); if(_accDevices) _accDevices.addEventListener('click', function(){ setView('devices'); });
|
||||
var _dvBack=document.getElementById('dvBack'); if(_dvBack) _dvBack.addEventListener('click', function(){ setView('account'); });
|
||||
var _routingRow=document.getElementById('routingRow'); if(_routingRow) _routingRow.addEventListener('click', function(){ setView('routing'); });
|
||||
var _routingRow=document.getElementById('routingRow'); if(_routingRow) _routingRow.addEventListener('click', function(){ setView('routing'); renderRouting(); });
|
||||
var _rtBack=document.getElementById('rtBack'); if(_rtBack) _rtBack.addEventListener('click', function(){ setView('settings'); });
|
||||
|
||||
/* ========= 分流规则:代理模式切换 + 添加/导入/重置 ========= */
|
||||
(function(){
|
||||
if(!S.routeMode) S.routeMode='smart';
|
||||
function _t(zh,en){ return S.lang==='zh'?zh:en; }
|
||||
function _esc(s){ return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>'); }
|
||||
var INP='padding:10px 12px;border-radius:var(--radius-md);border:1px solid var(--border);background:var(--surface);color:var(--fg1);font-size:14px;font-family:inherit;width:100%;box-sizing:border-box';
|
||||
|
||||
window.renderRouting=function(){
|
||||
var mode=S.routeMode||'smart', smart=mode==='smart';
|
||||
document.querySelectorAll('#rtModeSeg button').forEach(function(b){ b.classList.toggle('is-active', b.dataset.mode===mode); });
|
||||
['rtCnRow','rtMyRules'].forEach(function(id){ var el=document.getElementById(id); if(!el) return; el.style.opacity=smart?'':'.4'; el.style.pointerEvents=smart?'':'none'; });
|
||||
var fp=document.getElementById('rtFinalPill');
|
||||
if(fp){ if(mode==='direct'){ fp.textContent=_t('直连','Direct'); fp.className='pill is-success'; } else { fp.textContent=_t('走隧道','Tunnel'); fp.className='pill is-warning'; } }
|
||||
var note=document.getElementById('rtModeNote');
|
||||
if(note){
|
||||
if(smart) note.innerHTML=_t('下方「我的规则」仅在<b>智能分流</b>模式生效。全局代理 / 全部直连为一刀切模式,忽略自定义规则(系统强制层仍保留)。','Rules below apply only in <b>Smart routing</b>. Global / Direct are one-size modes that ignore custom rules (system-forced layer stays).');
|
||||
else if(mode==='global') note.innerHTML=_t('<b>全局代理</b>:所有流量走隧道,忽略国内直连与自定义规则(系统强制层仍保留)。','<b>Global proxy</b>: all traffic tunnels; China-direct and custom rules are ignored (system-forced layer stays).');
|
||||
else note.innerHTML=_t('<b>全部直连</b>:所有流量直连,忽略自定义规则(系统强制层仍保留)。','<b>Direct</b>: all traffic goes direct; custom rules are ignored (system-forced layer stays).');
|
||||
}
|
||||
};
|
||||
document.querySelectorAll('#rtModeSeg button').forEach(function(b){ b.addEventListener('click', function(){ S.routeMode=b.dataset.mode; renderRouting(); }); });
|
||||
|
||||
function openModal(id){ var m=document.getElementById(id); if(m){ m.classList.add('is-open'); m.onclick=function(e){ if(e.target===m) closeModal(id); }; } }
|
||||
function closeModal(id){ var m=document.getElementById(id); if(m) m.classList.remove('is-open'); }
|
||||
|
||||
function pillFor(act){
|
||||
if(act==='proxy') return '<span class="pill is-warning">'+_t('走隧道','Tunnel')+'</span>';
|
||||
if(act==='reject') return '<span class="pill is-danger">'+_t('拒绝','Reject')+'</span>';
|
||||
return '<span class="pill is-success">'+_t('直连','Direct')+'</span>';
|
||||
}
|
||||
var TYPE={domain_suffix:['域名后缀','Domain suffix'],domain:['域名','Domain'],ip_cidr:['IP-CIDR','IP-CIDR'],geosite:['GeoSite','GeoSite'],geoip:['GeoIP','GeoIP']};
|
||||
function addRuleRow(type,val,act,prepend){
|
||||
var list=document.getElementById('rtRulesList'); if(!list) return;
|
||||
var tl=(TYPE[type]||[type,type]); var typeLabel=_t(tl[0],tl[1]);
|
||||
var row=document.createElement('div'); row.className='setting-row';
|
||||
row.innerHTML='<span style="color:var(--fg3);opacity:.55;margin-right:var(--space-2)">≡</span><div class="st-body"><div class="st-title" style="font-family:var(--font-mono);font-size:var(--text-sm)">'+_esc(val)+'</div><div class="st-sub">'+typeLabel+'</div></div><span class="st-trail">'+pillFor(act)+'</span>';
|
||||
if(prepend) list.insertBefore(row,list.firstChild); else list.appendChild(row);
|
||||
}
|
||||
// 记住默认规则,供「重置默认」还原
|
||||
var _defaultRules=(function(){ var l=document.getElementById('rtRulesList'); return l?l.innerHTML:''; })();
|
||||
|
||||
// —— 添加规则 ——
|
||||
function openRtAdd(){
|
||||
var win=document.getElementById('rtAddWin');
|
||||
win.innerHTML='<button class="limit-close" id="rtAddClose" aria-label="'+_t('关闭','Close')+'"><svg><use href="#i-x"/></svg></button>'
|
||||
+'<div class="limit-title">'+_t('添加规则','Add rule')+'</div>'
|
||||
+'<div style="text-align:left;margin-top:18px;display:flex;flex-direction:column;gap:14px">'
|
||||
+'<label style="display:flex;flex-direction:column;gap:6px;font-size:13px;color:var(--fg2)"><span>'+_t('规则类型','Match type')+'</span>'
|
||||
+'<select id="rtAddType" style="'+INP+'"><option value="domain_suffix">'+_t('域名后缀','Domain suffix')+'</option><option value="domain">'+_t('域名(精确)','Domain (exact)')+'</option><option value="ip_cidr">IP-CIDR</option><option value="geosite">GeoSite '+_t('规则集','rule-set')+'</option><option value="geoip">GeoIP</option></select></label>'
|
||||
+'<label style="display:flex;flex-direction:column;gap:6px;font-size:13px;color:var(--fg2)"><span>'+_t('目标','Target')+'</span><input id="rtAddVal" style="'+INP+'" placeholder="'+_t('如 example.com','e.g. example.com')+'"></label>'
|
||||
+'<div style="display:flex;flex-direction:column;gap:6px"><span style="font-size:13px;color:var(--fg2)">'+_t('动作','Action')+'</span>'
|
||||
+'<div class="segswitch" id="rtActSeg"><button class="segswitch-opt is-active" data-act="direct"><span>'+_t('直连','Direct')+'</span></button><button class="segswitch-opt" data-act="proxy"><span>'+_t('走隧道','Tunnel')+'</span></button><button class="segswitch-opt" data-act="reject"><span>'+_t('拒绝','Reject')+'</span></button></div></div>'
|
||||
+'</div>'
|
||||
+'<div style="display:flex;gap:12px;margin-top:22px"><button class="btn btn-ghost" id="rtAddCancel" style="flex:1">'+_t('取消','Cancel')+'</button><button class="btn btn-subtle" id="rtAddOk" style="flex:1">'+_t('添加','Add')+'</button></div>';
|
||||
var act='direct';
|
||||
win.querySelectorAll('#rtActSeg button').forEach(function(b){ b.addEventListener('click',function(){ act=b.dataset.act; win.querySelectorAll('#rtActSeg button').forEach(function(x){ x.classList.toggle('is-active',x===b); }); }); });
|
||||
win.querySelector('#rtAddClose').addEventListener('click',function(){ closeModal('rtAddModal'); });
|
||||
win.querySelector('#rtAddCancel').addEventListener('click',function(){ closeModal('rtAddModal'); });
|
||||
win.querySelector('#rtAddOk').addEventListener('click',function(){ var v=(win.querySelector('#rtAddVal').value||'').trim(); if(!v){ win.querySelector('#rtAddVal').focus(); return; } addRuleRow(win.querySelector('#rtAddType').value,v,act,true); closeModal('rtAddModal'); });
|
||||
openModal('rtAddModal');
|
||||
}
|
||||
|
||||
// —— 从文本导入 ——
|
||||
function openRtImport(){
|
||||
var win=document.getElementById('rtImportWin');
|
||||
win.innerHTML='<button class="limit-close" id="rtImpClose" aria-label="'+_t('关闭','Close')+'"><svg><use href="#i-x"/></svg></button>'
|
||||
+'<div class="limit-title">'+_t('从文本导入','Import from text')+'</div>'
|
||||
+'<div class="limit-desc" style="margin-bottom:12px">'+_t('每行一条:目标,动作(direct / proxy / reject)','One per line: target,action (direct / proxy / reject)')+'</div>'
|
||||
+'<textarea id="rtImpText" rows="6" style="'+INP+';font-family:var(--font-mono);resize:vertical" placeholder="example.com,proxy ads.example.net,reject 10.0.0.0/8,direct"></textarea>'
|
||||
+'<div style="display:flex;gap:12px;margin-top:18px"><button class="btn btn-ghost" id="rtImpCancel" style="flex:1">'+_t('取消','Cancel')+'</button><button class="btn btn-subtle" id="rtImpOk" style="flex:1">'+_t('导入','Import')+'</button></div>';
|
||||
win.querySelector('#rtImpClose').addEventListener('click',function(){ closeModal('rtImportModal'); });
|
||||
win.querySelector('#rtImpCancel').addEventListener('click',function(){ closeModal('rtImportModal'); });
|
||||
win.querySelector('#rtImpOk').addEventListener('click',function(){
|
||||
var lines=(win.querySelector('#rtImpText').value||'').split('\n'); var n=0;
|
||||
lines.forEach(function(ln){ var p=ln.split(','); var v=(p[0]||'').trim(); var a=(p[1]||'proxy').trim().toLowerCase(); if(!v) return; var type=/^[\d.]+\/\d+$/.test(v)?'ip_cidr':'domain_suffix'; addRuleRow(type,v,a==='direct'?'direct':a==='reject'?'reject':'proxy',false); n++; });
|
||||
closeModal('rtImportModal');
|
||||
});
|
||||
openModal('rtImportModal');
|
||||
}
|
||||
|
||||
// —— 重置默认 ——
|
||||
function openRtReset(){
|
||||
var win=document.getElementById('rtResetWin');
|
||||
win.innerHTML='<button class="limit-close" id="rtRstClose" aria-label="'+_t('关闭','Close')+'"><svg><use href="#i-x"/></svg></button>'
|
||||
+'<svg class="limit-ic"><use href="#i-refresh-cw"/></svg>'
|
||||
+'<div class="limit-title">'+_t('重置为默认规则?','Reset to defaults?')+'</div>'
|
||||
+'<div class="limit-desc">'+_t('将清除你自定义的所有规则,恢复到出厂分流表。','Clears all your custom rules and restores the factory routing table.')+'</div>'
|
||||
+'<div style="display:flex;gap:12px;margin-top:22px"><button class="btn btn-ghost" id="rtRstCancel" style="flex:1">'+_t('取消','Cancel')+'</button><button class="btn btn-danger" id="rtRstOk" style="flex:1">'+_t('重置','Reset')+'</button></div>';
|
||||
win.querySelector('#rtRstClose').addEventListener('click',function(){ closeModal('rtResetModal'); });
|
||||
win.querySelector('#rtRstCancel').addEventListener('click',function(){ closeModal('rtResetModal'); });
|
||||
win.querySelector('#rtRstOk').addEventListener('click',function(){ var l=document.getElementById('rtRulesList'); if(l) l.innerHTML=_defaultRules; closeModal('rtResetModal'); });
|
||||
openModal('rtResetModal');
|
||||
}
|
||||
|
||||
var b1=document.getElementById('rtAddBtn'); if(b1) b1.addEventListener('click',openRtAdd);
|
||||
var b2=document.getElementById('rtImportBtn'); if(b2) b2.addEventListener('click',openRtImport);
|
||||
var b3=document.getElementById('rtResetBtn'); if(b3) b3.addEventListener('click',openRtReset);
|
||||
renderRouting();
|
||||
})();
|
||||
var _tbBell=document.getElementById('tbBell'); if(_tbBell) _tbBell.addEventListener('click', function(){ if(S.view!=='notifications'){ S.notifFrom=S.view; } setView('notifications'); });
|
||||
var _ntBack=document.getElementById('ntBack'); if(_ntBack) _ntBack.addEventListener('click', function(){ setView(S.notifFrom||'connect'); });
|
||||
document.querySelectorAll('.notif-row.is-expandable').forEach(function(row){
|
||||
|
||||
@@ -218,6 +218,12 @@
|
||||
#deviceLimitOverlay{ position:absolute; left:0; right:0; top:30px; bottom:0; z-index:45;
|
||||
background:var(--bg); display:none; flex-direction:column; }
|
||||
#deviceLimitOverlay.is-open{ display:flex; }
|
||||
/* 分流规则:添加/导入/重置 居中弹层 */
|
||||
.rt-sheet{ position:absolute; left:0; right:0; top:30px; bottom:0; z-index:60;
|
||||
background:rgba(30,20,12,.42); display:none; align-items:center; justify-content:center; padding:18px; }
|
||||
.rt-sheet.is-open{ display:flex; }
|
||||
.rt-win{ background:var(--surface); border-radius:var(--radius-lg); padding:20px; width:100%;
|
||||
max-width:340px; max-height:86%; overflow:auto; box-shadow:var(--shadow-lg); position:relative; }
|
||||
|
||||
/* 购买流程覆盖层(从账户页「兑换 & 购买」进入;移植 purchase.html 状态机,命名空间 .pur-*) */
|
||||
#purchaseOverlay{ position:absolute; left:0; right:0; top:30px; bottom:0; z-index:45;
|
||||
@@ -919,30 +925,32 @@
|
||||
<section class="sub-screen" data-sub="routing">
|
||||
<div class="sub-scr sub-stack">
|
||||
<div class="sec-label">代理模式</div>
|
||||
<div class="segswitch">
|
||||
<button class="segswitch-opt"><span>全局代理</span></button>
|
||||
<button class="segswitch-opt is-active"><span>智能分流</span></button>
|
||||
<button class="segswitch-opt"><span>全部直连</span></button>
|
||||
<div class="segswitch" id="rtModeSeg">
|
||||
<button class="segswitch-opt" data-mode="global"><span>全局代理</span></button>
|
||||
<button class="segswitch-opt is-active" data-mode="smart"><span>智能分流</span></button>
|
||||
<button class="segswitch-opt" data-mode="direct"><span>全部直连</span></button>
|
||||
</div>
|
||||
<p class="sub-note" style="text-align:left;margin:0">「我的规则」仅在<b>智能分流</b>模式生效;全局 / 直连为一刀切,忽略自定义规则(系统强制层仍保留)。</p>
|
||||
<p class="sub-note" id="rtModeNote" style="text-align:left;margin:0">「我的规则」仅在<b>智能分流</b>模式生效;全局 / 直连为一刀切,忽略自定义规则(系统强制层仍保留)。</p>
|
||||
<div class="sec-label">内置规则</div>
|
||||
<div class="card card-flush">
|
||||
<div class="setting-row"><span class="icon-box"><svg><use href="#i-globe"/></svg></span><div class="st-body"><div class="st-title">国内直连</div><div class="st-sub">GeoIP / GeoSite CN</div></div><div class="st-trail"><button class="toggle is-on"><i></i></button></div></div>
|
||||
<div class="setting-row" id="rtCnRow"><span class="icon-box"><svg><use href="#i-globe"/></svg></span><div class="st-body"><div class="st-title">国内直连</div><div class="st-sub">GeoIP / GeoSite CN</div></div><div class="st-trail"><button class="toggle is-on"><i></i></button></div></div>
|
||||
<div class="setting-row"><span class="icon-box"><svg><use href="#i-home"/></svg></span><div class="st-body"><div class="st-title" data-i18n="rr.lan">局域网 / 私网直连</div><div class="st-sub" data-i18n="rr.lanSub">强制,不可关</div></div><div class="st-trail"><span class="pill is-neutral" data-i18n="rr.forced">🔒</span></div></div>
|
||||
</div>
|
||||
<div id="rtMyRules">
|
||||
<div class="sec-label">我的规则 · 首命中生效</div>
|
||||
<div class="card card-flush">
|
||||
<div class="card card-flush" id="rtRulesList">
|
||||
<div class="setting-row"><span style="color:var(--fg3);opacity:.55">≡</span><div class="st-body"><div class="st-title" style="font-family:var(--font-mono);font-size:var(--text-sm)">git.51yanmei.com</div><div class="st-sub">域名后缀</div></div><div class="st-trail"><span class="pill is-success">直连</span></div></div>
|
||||
<div class="setting-row"><span style="color:var(--fg3);opacity:.55">≡</span><div class="st-body"><div class="st-title" style="font-family:var(--font-mono);font-size:var(--text-sm)">github.com</div><div class="st-sub">域名后缀</div></div><div class="st-trail"><span class="pill is-warning">走隧道</span></div></div>
|
||||
<div class="setting-row"><span style="color:var(--fg3);opacity:.55">≡</span><div class="st-body"><div class="st-title" style="font-family:var(--font-mono);font-size:var(--text-sm)">category-ads</div><div class="st-sub">GeoSite 规则集</div></div><div class="st-trail"><span class="pill is-danger">拒绝</span></div></div>
|
||||
</div>
|
||||
<button class="btn btn-subtle btn-block"><svg><use href="#i-plus"/></svg>添加规则</button>
|
||||
<button class="btn btn-subtle btn-block" id="rtAddBtn"><svg><use href="#i-plus"/></svg>添加规则</button>
|
||||
<div style="display:flex;gap:var(--space-2)">
|
||||
<button class="btn btn-ghost" style="flex:1"><svg><use href="#i-download"/></svg>从文本导入</button>
|
||||
<button class="btn btn-ghost" style="flex:1"><svg><use href="#i-refresh-cw"/></svg>重置默认</button>
|
||||
<button class="btn btn-ghost" id="rtImportBtn" style="flex:1"><svg><use href="#i-download"/></svg>从文本导入</button>
|
||||
<button class="btn btn-ghost" id="rtResetBtn" style="flex:1"><svg><use href="#i-refresh-cw"/></svg>重置默认</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="sec-label">最终(未命中)</div>
|
||||
<div class="card card-flush"><div class="setting-row"><div class="st-body"><div class="st-title">FINAL 兜底</div></div><div class="st-trail"><span class="pill is-warning">走隧道</span></div></div></div>
|
||||
<div class="card card-flush"><div class="setting-row"><div class="st-body"><div class="st-title">FINAL 兜底</div></div><div class="st-trail"><span class="pill is-warning" id="rtFinalPill">走隧道</span></div></div></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -1167,6 +1175,10 @@
|
||||
</div>
|
||||
|
||||
<!-- 登录后设备超限挡板 overlay(根级、盖满 App 窗口;演示控制「挡板 · 设备超限」触发) -->
|
||||
<!-- 分流规则:添加 / 导入 / 重置 弹层(内容由 JS 双语渲染) -->
|
||||
<div class="rt-sheet" id="rtAddModal"><div class="rt-win" id="rtAddWin"></div></div>
|
||||
<div class="rt-sheet" id="rtImportModal"><div class="rt-win" id="rtImportWin"></div></div>
|
||||
<div class="rt-sheet" id="rtResetModal"><div class="rt-win" id="rtResetWin"></div></div>
|
||||
<div id="deviceLimitOverlay">
|
||||
<div class="dl-body">
|
||||
<div class="dl-hero">
|
||||
@@ -2433,7 +2445,85 @@ document.querySelectorAll('.notif-row.is-expandable').forEach(function(row){
|
||||
});
|
||||
// 账户入口
|
||||
$('#rowSettings').addEventListener('click', ()=>subOpen('settings'));
|
||||
$('#rowRouting').addEventListener('click', ()=>subOpen('routing'));
|
||||
$('#rowRouting').addEventListener('click', ()=>{ subOpen('routing'); renderRouting(); });
|
||||
|
||||
/* ========= 分流规则:代理模式切换 + 添加/导入/重置 ========= */
|
||||
(function(){
|
||||
if(!state.routeMode) state.routeMode='smart';
|
||||
const _t=(zh,en)=>state.lang==='zh'?zh:en;
|
||||
const _esc=s=>String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');
|
||||
const INP='padding:10px 12px;border-radius:var(--radius-md);border:1px solid var(--border);background:var(--bg-subtle);color:var(--fg1);font-size:14px;font-family:inherit;width:100%;box-sizing:border-box';
|
||||
|
||||
window.renderRouting=function(){
|
||||
const mode=state.routeMode||'smart', smart=mode==='smart';
|
||||
document.querySelectorAll('#rtModeSeg button').forEach(b=>b.classList.toggle('is-active', b.dataset.mode===mode));
|
||||
['rtCnRow','rtMyRules'].forEach(id=>{ const el=document.getElementById(id); if(!el) return; el.style.opacity=smart?'':'.4'; el.style.pointerEvents=smart?'':'none'; });
|
||||
const fp=document.getElementById('rtFinalPill');
|
||||
if(fp){ if(mode==='direct'){ fp.textContent=_t('直连','Direct'); fp.className='pill is-success'; } else { fp.textContent=_t('走隧道','Tunnel'); fp.className='pill is-warning'; } }
|
||||
const note=document.getElementById('rtModeNote');
|
||||
if(note){
|
||||
if(smart) note.innerHTML=_t('「我的规则」仅在<b>智能分流</b>模式生效;全局 / 直连为一刀切,忽略自定义规则(系统强制层仍保留)。','Rules apply only in <b>Smart routing</b>; Global / Direct are one-size modes that ignore custom rules (system-forced layer stays).');
|
||||
else if(mode==='global') note.innerHTML=_t('<b>全局代理</b>:所有流量走隧道,忽略国内直连与自定义规则(系统强制层仍保留)。','<b>Global proxy</b>: all traffic tunnels; China-direct and custom rules ignored (system-forced layer stays).');
|
||||
else note.innerHTML=_t('<b>全部直连</b>:所有流量直连,忽略自定义规则(系统强制层仍保留)。','<b>Direct</b>: all traffic goes direct; custom rules ignored (system-forced layer stays).');
|
||||
}
|
||||
};
|
||||
document.querySelectorAll('#rtModeSeg button').forEach(b=>b.addEventListener('click', ()=>{ state.routeMode=b.dataset.mode; renderRouting(); }));
|
||||
|
||||
const openM=id=>{ const m=document.getElementById(id); if(m){ m.classList.add('is-open'); m.onclick=e=>{ if(e.target===m) m.classList.remove('is-open'); }; } };
|
||||
const closeM=id=>{ const m=document.getElementById(id); if(m) m.classList.remove('is-open'); };
|
||||
const pillFor=act=> act==='proxy'?'<span class="pill is-warning">'+_t('走隧道','Tunnel')+'</span>' : act==='reject'?'<span class="pill is-danger">'+_t('拒绝','Reject')+'</span>' : '<span class="pill is-success">'+_t('直连','Direct')+'</span>';
|
||||
const TYPE={domain_suffix:['域名后缀','Domain suffix'],domain:['域名','Domain'],ip_cidr:['IP-CIDR','IP-CIDR'],geosite:['GeoSite','GeoSite'],geoip:['GeoIP','GeoIP']};
|
||||
function addRuleRow(type,val,act,prepend){
|
||||
const list=document.getElementById('rtRulesList'); if(!list) return;
|
||||
const tl=TYPE[type]||[type,type];
|
||||
const row=document.createElement('div'); row.className='setting-row';
|
||||
row.innerHTML='<span style="color:var(--fg3);opacity:.55">≡</span><div class="st-body"><div class="st-title" style="font-family:var(--font-mono);font-size:var(--text-sm)">'+_esc(val)+'</div><div class="st-sub">'+_t(tl[0],tl[1])+'</div></div><div class="st-trail">'+pillFor(act)+'</div>';
|
||||
if(prepend) list.insertBefore(row,list.firstChild); else list.appendChild(row);
|
||||
}
|
||||
const _defaultRules=(()=>{ const l=document.getElementById('rtRulesList'); return l?l.innerHTML:''; })();
|
||||
|
||||
function openAdd(){
|
||||
const win=document.getElementById('rtAddWin');
|
||||
win.innerHTML='<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:16px"><b style="font-family:var(--font-display);font-size:18px">'+_t('添加规则','Add rule')+'</b><button class="btn btn-ghost" id="rtAddClose" style="padding:6px"><svg style="width:20px;height:20px"><use href="#i-x"/></svg></button></div>'
|
||||
+'<div style="display:flex;flex-direction:column;gap:14px">'
|
||||
+'<label style="display:flex;flex-direction:column;gap:6px;font-size:13px;color:var(--fg2)"><span>'+_t('规则类型','Match type')+'</span><select id="rtAddType" style="'+INP+'"><option value="domain_suffix">'+_t('域名后缀','Domain suffix')+'</option><option value="domain">'+_t('域名(精确)','Domain (exact)')+'</option><option value="ip_cidr">IP-CIDR</option><option value="geosite">GeoSite '+_t('规则集','rule-set')+'</option><option value="geoip">GeoIP</option></select></label>'
|
||||
+'<label style="display:flex;flex-direction:column;gap:6px;font-size:13px;color:var(--fg2)"><span>'+_t('目标','Target')+'</span><input id="rtAddVal" style="'+INP+'" placeholder="'+_t('如 example.com','e.g. example.com')+'"></label>'
|
||||
+'<div style="display:flex;flex-direction:column;gap:6px"><span style="font-size:13px;color:var(--fg2)">'+_t('动作','Action')+'</span><div class="segswitch" id="rtActSeg"><button class="segswitch-opt is-active" data-act="direct"><span>'+_t('直连','Direct')+'</span></button><button class="segswitch-opt" data-act="proxy"><span>'+_t('走隧道','Tunnel')+'</span></button><button class="segswitch-opt" data-act="reject"><span>'+_t('拒绝','Reject')+'</span></button></div></div>'
|
||||
+'</div>'
|
||||
+'<div style="display:flex;gap:10px;margin-top:20px"><button class="btn btn-ghost" id="rtAddCancel" style="flex:1">'+_t('取消','Cancel')+'</button><button class="btn btn-subtle" id="rtAddOk" style="flex:1">'+_t('添加','Add')+'</button></div>';
|
||||
let act='direct';
|
||||
win.querySelectorAll('#rtActSeg button').forEach(b=>b.addEventListener('click',()=>{ act=b.dataset.act; win.querySelectorAll('#rtActSeg button').forEach(x=>x.classList.toggle('is-active',x===b)); }));
|
||||
win.querySelector('#rtAddClose').addEventListener('click',()=>closeM('rtAddModal'));
|
||||
win.querySelector('#rtAddCancel').addEventListener('click',()=>closeM('rtAddModal'));
|
||||
win.querySelector('#rtAddOk').addEventListener('click',()=>{ const v=(win.querySelector('#rtAddVal').value||'').trim(); if(!v){ win.querySelector('#rtAddVal').focus(); return; } addRuleRow(win.querySelector('#rtAddType').value,v,act,true); closeM('rtAddModal'); });
|
||||
openM('rtAddModal');
|
||||
}
|
||||
function openImport(){
|
||||
const win=document.getElementById('rtImportWin');
|
||||
win.innerHTML='<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:12px"><b style="font-family:var(--font-display);font-size:18px">'+_t('从文本导入','Import from text')+'</b><button class="btn btn-ghost" id="rtImpClose" style="padding:6px"><svg style="width:20px;height:20px"><use href="#i-x"/></svg></button></div>'
|
||||
+'<div style="font-size:13px;color:var(--fg2);margin-bottom:10px">'+_t('每行一条:目标,动作(direct / proxy / reject)','One per line: target,action (direct / proxy / reject)')+'</div>'
|
||||
+'<textarea id="rtImpText" rows="6" style="'+INP+';font-family:var(--font-mono);resize:vertical" placeholder="example.com,proxy ads.example.net,reject 10.0.0.0/8,direct"></textarea>'
|
||||
+'<div style="display:flex;gap:10px;margin-top:16px"><button class="btn btn-ghost" id="rtImpCancel" style="flex:1">'+_t('取消','Cancel')+'</button><button class="btn btn-subtle" id="rtImpOk" style="flex:1">'+_t('导入','Import')+'</button></div>';
|
||||
win.querySelector('#rtImpClose').addEventListener('click',()=>closeM('rtImportModal'));
|
||||
win.querySelector('#rtImpCancel').addEventListener('click',()=>closeM('rtImportModal'));
|
||||
win.querySelector('#rtImpOk').addEventListener('click',()=>{ (win.querySelector('#rtImpText').value||'').split('\n').forEach(ln=>{ const p=ln.split(','); const v=(p[0]||'').trim(); const a=(p[1]||'proxy').trim().toLowerCase(); if(!v) return; const type=/^[\d.]+\/\d+$/.test(v)?'ip_cidr':'domain_suffix'; addRuleRow(type,v,a==='direct'?'direct':a==='reject'?'reject':'proxy',false); }); closeM('rtImportModal'); });
|
||||
openM('rtImportModal');
|
||||
}
|
||||
function openReset(){
|
||||
const win=document.getElementById('rtResetWin');
|
||||
win.innerHTML='<div style="text-align:center"><svg style="width:40px;height:40px;color:var(--warning);margin-bottom:8px"><use href="#i-refresh-cw"/></svg>'
|
||||
+'<div style="font-family:var(--font-display);font-size:18px;font-weight:700;margin-bottom:8px">'+_t('重置为默认规则?','Reset to defaults?')+'</div>'
|
||||
+'<div style="font-size:14px;color:var(--fg2);line-height:1.5">'+_t('将清除你自定义的所有规则,恢复到出厂分流表。','Clears all your custom rules and restores the factory routing table.')+'</div></div>'
|
||||
+'<div style="display:flex;gap:10px;margin-top:20px"><button class="btn btn-ghost" id="rtRstCancel" style="flex:1">'+_t('取消','Cancel')+'</button><button class="btn btn-danger" id="rtRstOk" style="flex:1">'+_t('重置','Reset')+'</button></div>';
|
||||
win.querySelector('#rtRstCancel').addEventListener('click',()=>closeM('rtResetModal'));
|
||||
win.querySelector('#rtRstOk').addEventListener('click',()=>{ const l=document.getElementById('rtRulesList'); if(l) l.innerHTML=_defaultRules; closeM('rtResetModal'); });
|
||||
openM('rtResetModal');
|
||||
}
|
||||
const b1=document.getElementById('rtAddBtn'); if(b1) b1.addEventListener('click',openAdd);
|
||||
const b2=document.getElementById('rtImportBtn'); if(b2) b2.addEventListener('click',openImport);
|
||||
const b3=document.getElementById('rtResetBtn'); if(b3) b3.addEventListener('click',openReset);
|
||||
renderRouting();
|
||||
})();
|
||||
$('#rowContact').addEventListener('click', ()=>subOpen('contact'));
|
||||
$('#rowInvite').addEventListener('click', ()=>subOpen('invite'));
|
||||
$('#rowRedeem').addEventListener('click', ()=>subOpen('redeem'));
|
||||
|
||||
Reference in New Issue
Block a user