From 4a912b5af3ab8abfc9a6bf18c43a795ab6770d6f Mon Sep 17 00:00:00 2001 From: wangjia <809946525@qq.com> Date: Mon, 27 Jul 2026 10:35:49 +0800 Subject: [PATCH] =?UTF-8?q?feat(proxy):=20=E5=88=86=E6=B5=81=E8=A7=84?= =?UTF-8?q?=E5=88=99=E9=A1=B5=E4=BA=A4=E4=BA=92=20=E2=80=94=20=E4=BB=A3?= =?UTF-8?q?=E7=90=86=E6=A8=A1=E5=BC=8F=E5=88=87=E6=8D=A2=20+=20=E6=B7=BB?= =?UTF-8?q?=E5=8A=A0/=E5=AF=BC=E5=85=A5/=E9=87=8D=E7=BD=AE(=E4=B8=A4?= =?UTF-8?q?=E7=AB=AF)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 原型路由页此前段选与三按钮无事件、不重画。补齐(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 Claude-Session: https://claude.ai/code/session_01A79VtQA1BwTuQN1ThpvYpo --- design/prototype/screens/ui-desktop.html | 132 ++++++++++++++++++++--- design/prototype/screens/ui-mobile.html | 114 +++++++++++++++++--- 2 files changed, 221 insertions(+), 25 deletions(-) diff --git a/design/prototype/screens/ui-desktop.html b/design/prototype/screens/ui-desktop.html index 0eb63aa..31489b8 100644 --- a/design/prototype/screens/ui-desktop.html +++ b/design/prototype/screens/ui-desktop.html @@ -740,19 +740,20 @@
代理模式
-
- - - +
+ + +
-

下方「我的规则」仅在智能分流模式生效。全局代理 / 全部直连为一刀切模式,忽略自定义规则(系统强制层仍保留)。

+

下方「我的规则」仅在智能分流模式生效。全局代理 / 全部直连为一刀切模式,忽略自定义规则(系统强制层仍保留)。

内置规则
-
国内直连
GeoIP / GeoSite CN
+
国内直连
GeoIP / GeoSite CN
局域网 / 私网直连
强制,不可关
🔒 强制
-
我的规则 · 有序,首命中生效
-
+
+
我的规则 · 有序,首命中生效
+
git.51yanmei.com
域名后缀 · CI 源
直连
aliyun.com
域名后缀 · 镜像
直连
github.com
域名后缀
走隧道
@@ -760,12 +761,13 @@
git.yanmeiai.com
⚠ 该域名由系统强制走隧道,此规则不生效
直连
- - - + + + +
最终(未命中任何规则)
-
FINAL 兜底
走隧道
+
FINAL 兜底
走隧道
@@ -1119,6 +1121,10 @@
+ +
+
+
@@ -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,'>'); } + 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('下方「我的规则」仅在智能分流模式生效。全局代理 / 全部直连为一刀切模式,忽略自定义规则(系统强制层仍保留)。','Rules below apply only in Smart routing. Global / Direct are one-size modes that ignore custom rules (system-forced layer stays).'); + else if(mode==='global') note.innerHTML=_t('全局代理:所有流量走隧道,忽略国内直连与自定义规则(系统强制层仍保留)。','Global proxy: all traffic tunnels; China-direct and custom rules are ignored (system-forced layer stays).'); + else note.innerHTML=_t('全部直连:所有流量直连,忽略自定义规则(系统强制层仍保留)。','Direct: 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 ''+_t('走隧道','Tunnel')+''; + if(act==='reject') return ''+_t('拒绝','Reject')+''; + return ''+_t('直连','Direct')+''; + } + 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='
'+_esc(val)+'
'+typeLabel+'
'+pillFor(act)+''; + 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='' + +'
'+_t('添加规则','Add rule')+'
' + +'
' + +'' + +'' + +'
'+_t('动作','Action')+'' + +'
' + +'
' + +'
'; + 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='' + +'
'+_t('从文本导入','Import from text')+'
' + +'
'+_t('每行一条:目标,动作(direct / proxy / reject)','One per line: target,action (direct / proxy / reject)')+'
' + +'' + +'
'; + 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='' + +'' + +'
'+_t('重置为默认规则?','Reset to defaults?')+'
' + +'
'+_t('将清除你自定义的所有规则,恢复到出厂分流表。','Clears all your custom rules and restores the factory routing table.')+'
' + +'
'; + 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){ diff --git a/design/prototype/screens/ui-mobile.html b/design/prototype/screens/ui-mobile.html index 1aab054..8593dd0 100644 --- a/design/prototype/screens/ui-mobile.html +++ b/design/prototype/screens/ui-mobile.html @@ -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 @@
代理模式
-
- - - +
+ + +
-

「我的规则」仅在智能分流模式生效;全局 / 直连为一刀切,忽略自定义规则(系统强制层仍保留)。

+

「我的规则」仅在智能分流模式生效;全局 / 直连为一刀切,忽略自定义规则(系统强制层仍保留)。

内置规则
-
国内直连
GeoIP / GeoSite CN
+
国内直连
GeoIP / GeoSite CN
局域网 / 私网直连
强制,不可关
🔒
+
我的规则 · 首命中生效
-
+
git.51yanmei.com
域名后缀
直连
github.com
域名后缀
走隧道
category-ads
GeoSite 规则集
拒绝
- +
- - + + +
最终(未命中)
-
FINAL 兜底
走隧道
+
FINAL 兜底
走隧道
@@ -1167,6 +1175,10 @@ + +
+
+
@@ -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,'>'); + 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('「我的规则」仅在智能分流模式生效;全局 / 直连为一刀切,忽略自定义规则(系统强制层仍保留)。','Rules apply only in Smart routing; Global / Direct are one-size modes that ignore custom rules (system-forced layer stays).'); + else if(mode==='global') note.innerHTML=_t('全局代理:所有流量走隧道,忽略国内直连与自定义规则(系统强制层仍保留)。','Global proxy: all traffic tunnels; China-direct and custom rules ignored (system-forced layer stays).'); + else note.innerHTML=_t('全部直连:所有流量直连,忽略自定义规则(系统强制层仍保留)。','Direct: 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'?''+_t('走隧道','Tunnel')+'' : act==='reject'?''+_t('拒绝','Reject')+'' : ''+_t('直连','Direct')+''; + 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='
'+_esc(val)+'
'+_t(tl[0],tl[1])+'
'+pillFor(act)+'
'; + 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='
'+_t('添加规则','Add rule')+'
' + +'
' + +'' + +'' + +'
'+_t('动作','Action')+'
' + +'
' + +'
'; + 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='
'+_t('从文本导入','Import from text')+'
' + +'
'+_t('每行一条:目标,动作(direct / proxy / reject)','One per line: target,action (direct / proxy / reject)')+'
' + +'' + +'
'; + 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='
' + +'
'+_t('重置为默认规则?','Reset to defaults?')+'
' + +'
'+_t('将清除你自定义的所有规则,恢复到出厂分流表。','Clears all your custom rules and restores the factory routing table.')+'
' + +'
'; + 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'));